using Distribution.Features.GeneralLedger; using Distribution.Features.Stock; using Distribution.Infra.Authorization; using Distribution.Infra.UoW; using Distribution.Models; namespace Distribution.Features.Invoice; public class InvoiceService( IInvoiceRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, GlPostingService gl) { 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 GetByIdAsync(Guid id) { var invoice = await repo.GetByIdAsync(id); return invoice is null ? null : ToResponse(invoice); } public async Task> 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(page, pageSize, total, items.Select(ToResponse).ToList()); } public async Task 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 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 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; } await gl.PostInvoiceFinalizedAsync(invoice, ct); return ToResponse(invoice); } }