mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-04 13:42:03 +00:00
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.
143 lines
5.1 KiB
C#
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
|
|
);
|
|
}
|