proposal-system/api/src/ProposalSystem.Infrastructure/Services/LineItemService.cs
Adam Moussa 9b97b7b578 Fix proposal workflow: scoped My Proposals, idempotent state transitions
Add Mine filter to ProposalFilterRequest so My Proposals page shows
only the current user's submissions regardless of role. Create
proposals directly as InReview (skip Draft) since form submission is
the review request. Make Approve, MarkSent, and Revise idempotent —
repeat calls return current state instead of throwing. Wrap line item
audit logging in try/catch so audit failures don't mask successful
saves.
2026-05-20 17:24:09 -04:00

143 lines
5.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using ProposalSystem.Application.DTOs;
using ProposalSystem.Application.Interfaces;
using ProposalSystem.Domain.Entities;
using ProposalSystem.Infrastructure.Data;
namespace ProposalSystem.Infrastructure.Services;
public class LineItemService : ILineItemService
{
private readonly ProposalDbContext _db;
private readonly IAuditService _audit;
public LineItemService(ProposalDbContext db, IAuditService audit)
{
_db = db;
_audit = audit;
}
public async Task<IReadOnlyList<LineItemResponse>> GetByProposalIdAsync(Guid proposalId, CancellationToken ct = default)
{
return await _db.LineItems
.Where(li => li.ProposalId == proposalId)
.OrderBy(li => li.SortOrder)
.Select(li => MapToResponse(li))
.ToListAsync(ct);
}
public async Task<LineItemResponse> CreateAsync(Guid proposalId, CreateLineItemRequest request, CancellationToken ct = default)
{
var proposal = await _db.Proposals.FindAsync(new object[] { proposalId }, ct)
?? throw new KeyNotFoundException($"Proposal {proposalId} not found");
if (proposal.Status == ProposalStatus.Approved || proposal.Status == ProposalStatus.Sent)
throw new InvalidOperationException("Cannot modify line items on approved/sent proposals");
var now = DateTime.UtcNow;
var lineItem = new LineItem
{
Id = Guid.NewGuid(),
ProposalId = proposalId,
Description = request.Description,
Quantity = request.Quantity,
Unit = request.Unit,
UnitPrice = request.UnitPrice,
TotalPrice = request.TotalPrice,
PricingMode = request.PricingMode,
SortOrder = request.SortOrder,
Source = request.Source,
CreatedAt = now,
UpdatedAt = now,
};
_db.LineItems.Add(lineItem);
await _db.SaveChangesAsync(ct);
try
{
await _audit.LogAsync(AuditAction.EditLineItem, proposalId, $"Added: {request.Description}", ct);
}
catch { /* audit failure should not roll back a successful save */ }
return MapToResponse(lineItem);
}
public async Task<IReadOnlyList<LineItemResponse>> BulkUpdateAsync(Guid proposalId, BulkUpdateLineItemsRequest request, CancellationToken ct = default)
{
var proposal = await _db.Proposals.FindAsync(new object[] { proposalId }, ct)
?? throw new KeyNotFoundException($"Proposal {proposalId} not found");
if (proposal.Status == ProposalStatus.Approved || proposal.Status == ProposalStatus.Sent)
throw new InvalidOperationException("Cannot modify line items on approved/sent proposals");
var existing = await _db.LineItems
.Where(li => li.ProposalId == proposalId)
.ToListAsync(ct);
_db.LineItems.RemoveRange(existing);
var now = DateTime.UtcNow;
var newItems = request.LineItems.Select(entry => new LineItem
{
Id = entry.Id ?? Guid.NewGuid(),
ProposalId = proposalId,
Description = entry.Description,
Quantity = entry.Quantity,
Unit = entry.Unit,
UnitPrice = entry.UnitPrice,
TotalPrice = entry.TotalPrice,
PricingMode = entry.PricingMode,
SortOrder = entry.SortOrder,
Source = entry.Source,
CreatedAt = now,
UpdatedAt = now,
}).ToList();
_db.LineItems.AddRange(newItems);
proposal.TotalBidAmount = newItems.Sum(li => li.TotalPrice);
proposal.UpdatedAt = now;
await _db.SaveChangesAsync(ct);
try
{
await _audit.LogAsync(AuditAction.EditLineItem, proposalId, $"Bulk update: {newItems.Count} items", ct);
}
catch { /* audit failure should not roll back a successful save */ }
return newItems.OrderBy(li => li.SortOrder).Select(MapToResponse).ToList();
}
public async Task DeleteAsync(Guid proposalId, Guid lineItemId, CancellationToken ct = default)
{
var lineItem = await _db.LineItems
.FirstOrDefaultAsync(li => li.Id == lineItemId && li.ProposalId == proposalId, ct)
?? throw new KeyNotFoundException($"Line item {lineItemId} not found");
var proposal = await _db.Proposals.FindAsync(new object[] { proposalId }, ct)!;
if (proposal!.Status == ProposalStatus.Approved || proposal.Status == ProposalStatus.Sent)
throw new InvalidOperationException("Cannot modify line items on approved/sent proposals");
_db.LineItems.Remove(lineItem);
await _db.SaveChangesAsync(ct);
await _audit.LogAsync(AuditAction.EditLineItem, proposalId, $"Removed: {lineItem.Description}", ct);
}
private static LineItemResponse MapToResponse(LineItem li) => new(
li.Id,
li.ProposalId,
li.Description,
li.Quantity,
li.Unit,
li.UnitPrice,
li.TotalPrice,
li.PricingMode,
li.SortOrder,
li.Source,
li.CreatedAt,
li.UpdatedAt
);
}