Files
ERP_Distribution/Distribution/Features/Invoice/InvoiceService.cs
T
Kalana B. Thilakarathna 2de9e5e4ad
CI/CD - ERP Distribution / Build & Deploy Frontend (pull_request) Failing after 13m13s
CI/CD - ERP Distribution / Build & Deploy Backend (pull_request) Failing after 14m28s
Add manual invoice creation mode with new API & UI flow
Introduced "ModeB" for manual invoice creation in system config. Added backend endpoints and service logic for creating invoices from confirmed Shop POs, with validation and error handling. Updated DTOs to support optional CreatedByUserId. On the frontend, added configuration card to toggle modes, a dialog for manual invoice creation, and UI updates to support the new workflow. Enables admins to choose between automatic and manual invoice creation.
2026-10-20 13:38:43 +05:30

139 lines
5.4 KiB
C#

using Distribution.Features.ShopPO;
using Distribution.Features.Stock;
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
using Distribution.Models;
namespace Distribution.Features.Invoice;
public class InvoiceService(
IInvoiceRepository repo,
IShopPORepository shopPoRepo,
IUnitOfWork uow,
ICurrentUserService currentUser)
{
private static InvoiceResponse ToResponse(Models.Invoice i) =>
new(i.Id, i.FK_ShopPOId,
i.ShopPO?.FK_StoreId ?? Guid.Empty,
i.ShopPO?.Store?.StoreName ?? string.Empty,
i.Status, i.PaymentStatus, i.CreatedBy,
i.PrintedAt, i.PrintedBy,
i.FinalizedAt, i.FinalizedBy,
i.TotalAmount,
i.Payments?.Where(p => p.IsActive).Sum(p => p.Amount) ?? 0m,
i.IsActive, i.CreatedAt);
public async Task<InvoiceResponse?> GetByIdAsync(Guid id)
{
var invoice = await repo.GetByIdAsync(id);
return invoice is null ? null : ToResponse(invoice);
}
public async Task<PagedResponse<InvoiceResponse>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize)
{
var total = await repo.GetCountAsync(agentId, routeId, shopId, status, paymentStatus);
var items = await repo.GetAllAsync(agentId, routeId, shopId, status, paymentStatus, page, pageSize);
return new PagedResponse<InvoiceResponse>(page, pageSize, total, items.Select(ToResponse).ToList());
}
public async Task<OutstandingPaymentsResponse> GetOutstandingSummaryAsync()
{
var invoices = await repo.GetOutstandingAsync();
var count = invoices.Count;
var outstanding = invoices.Sum(i => i.TotalAmount - i.Payments.Sum(p => p.Amount));
return new OutstandingPaymentsResponse(count, outstanding);
}
public async Task<InvoiceResponse> CreateAsync(CreateInvoiceRequest request, Guid createdBy, CancellationToken ct = default)
{
var po = await shopPoRepo.GetByIdAsync(request.ShopPOId)
?? throw new KeyNotFoundException("Shop PO not found");
if (po.ConfirmedAt is null)
throw new InvalidOperationException("Shop PO must be confirmed before an invoice can be created");
if (po.Status == ShopPOStatus.Cancelled)
throw new InvalidOperationException("Cannot create an invoice for a cancelled Shop PO");
if (po.InvoiceId is not null)
throw new InvalidOperationException("An invoice already exists for this Shop PO");
var invoice = new Models.Invoice
{
FK_ShopPOId = po.Id,
Status = InvoiceStatus.Draft,
PaymentStatus = PaymentStatus.Pending,
CreatedBy = createdBy,
TotalAmount = po.TotalAmount
};
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(invoice);
po.InvoiceId = invoice.Id;
shopPoRepo.Update(po);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
var saved = await repo.GetByIdAsync(invoice.Id);
return ToResponse(saved!);
}
public async Task<InvoiceResponse> PrintAsync(Guid id, Guid printedBy, CancellationToken ct = default)
{
var invoice = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Invoice not found");
if (invoice.Status != InvoiceStatus.Draft)
throw new InvalidOperationException($"Cannot print an invoice in {invoice.Status} status");
invoice.Status = InvoiceStatus.Printed;
invoice.PrintedAt = DateTime.Now;
invoice.PrintedBy = printedBy;
await uow.BeginAsync(ct);
try { repo.Update(invoice); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(invoice);
}
public async Task<InvoiceResponse> FinalizeAsync(Guid id, FinalizeInvoiceRequest request, Guid finalizedBy, CancellationToken ct = default)
{
var invoice = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Invoice not found");
if (invoice.Status != InvoiceStatus.Printed)
throw new InvalidOperationException($"Cannot finalize an invoice in {invoice.Status} status");
var adjustments = request.Adjustments ?? [];
if (adjustments.Count > 0)
{
var poItems = invoice.ShopPO.Items.Where(i => i.IsActive).ToList();
decimal total = 0;
foreach (var item in poItems)
{
var adjustment = adjustments.FirstOrDefault(a => a.ShopPOItemId == item.Id);
var billedQuantity = adjustment is not null ? adjustment.AdjustedQuantity : item.Quantity;
if (billedQuantity < 0 || billedQuantity > item.Quantity)
throw new InvalidOperationException(
$"Adjusted quantity for item {item.Id} must be between 0 and {item.Quantity}");
total += billedQuantity * item.UnitPrice;
}
invoice.TotalAmount = total;
}
invoice.Status = InvoiceStatus.Finalized;
invoice.FinalizedAt = DateTime.Now;
invoice.FinalizedBy = finalizedBy;
await uow.BeginAsync(ct);
try { repo.Update(invoice); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(invoice);
}
}