mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 13:33:15 +00:00
136 lines
4.9 KiB
C#
136 lines
4.9 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);
|
||
|
|
|
||
|
|
await _audit.LogAsync(AuditAction.EditLineItem, proposalId, $"Added: {request.Description}", ct);
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
await _audit.LogAsync(AuditAction.EditLineItem, proposalId, $"Bulk update: {newItems.Count} items", ct);
|
||
|
|
|
||
|
|
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
|
||
|
|
);
|
||
|
|
}
|