From e5dadef8635e6ef60c1cfffc96f5f1a630e274e5 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 22 Sep 2026 16:58:38 +0530 Subject: [PATCH] Add invoice print and finalize flow --- .../DeliveryRide/DeliveryTaskService.cs | 50 +++++++++++++++- .../Features/Invoice/InvoiceController.cs | 29 +++++++++- Distribution/Features/Invoice/InvoiceDTOs.cs | 2 +- .../Features/Invoice/InvoiceRepository.cs | 1 + .../Features/Invoice/InvoiceService.cs | 57 +++++++++++++++++++ 5 files changed, 135 insertions(+), 4 deletions(-) diff --git a/Distribution/Features/DeliveryRide/DeliveryTaskService.cs b/Distribution/Features/DeliveryRide/DeliveryTaskService.cs index 5f1f336..18596fa 100644 --- a/Distribution/Features/DeliveryRide/DeliveryTaskService.cs +++ b/Distribution/Features/DeliveryRide/DeliveryTaskService.cs @@ -1,3 +1,4 @@ +using Distribution.Features.LoadingEvent; using Distribution.Features.Ride; using Distribution.Features.ShopPO; using Distribution.Features.Stock; @@ -10,6 +11,7 @@ public class DeliveryTaskService( IDeliveryTaskRepository repo, IRideRepository rideRepo, IShopPORepository shopPORepo, + ILoadingSheetRepository loadingSheetRepo, IStockRepository stockRepo, IUnitOfWork uow) { @@ -32,8 +34,52 @@ public class DeliveryTaskService( i.UnitPrice)).ToList(), t.CreatedAt); - public Task> GenerateAsync(Guid rideId, CancellationToken ct = default) => - throw new NotImplementedException("Delivery task auto-generation is deferred — route-based dispatch not yet implemented."); + public async Task> GenerateAsync(Guid rideId, CancellationToken ct = default) + { + var ride = await rideRepo.GetByIdAsync(rideId) + ?? throw new KeyNotFoundException("Ride not found"); + + var confirmedSheets = await loadingSheetRepo.GetAllAsync(rideId, LoadingSheetStatus.Confirmed, null, null, 1, int.MaxValue); + if (confirmedSheets.Count == 0) + throw new InvalidOperationException("Ride has no confirmed loading sheet to generate delivery tasks from"); + + var shopPOIds = confirmedSheets + .SelectMany(s => s.Orders) + .Select(o => o.FK_ShopPOId) + .Distinct() + .ToList(); + + var existingTasks = await repo.GetByRideIdAsync(rideId); + var nextSequence = existingTasks.Count > 0 ? existingTasks.Max(t => t.Sequence) + 1 : 1; + + var newTasks = new List(); + foreach (var shopPOId in shopPOIds) + { + if (await repo.ExistsForOrderAndRideAsync(shopPOId, rideId)) continue; + + newTasks.Add(new DeliveryTask + { + FK_RideId = rideId, + FK_ShopPOId = shopPOId, + Sequence = nextSequence++, + Status = DeliveryTaskStatus.Pending + }); + } + + if (newTasks.Count > 0) + { + await uow.BeginAsync(ct); + try + { + foreach (var task in newTasks) await repo.AddAsync(task); + await uow.CommitAsync(ct); + } + catch { await uow.RollbackAsync(ct); throw; } + } + + var saved = await repo.GetByRideIdAsync(rideId); + return saved.Select(ToResponse).ToList(); + } public async Task> GetByRideIdAsync(Guid rideId) { diff --git a/Distribution/Features/Invoice/InvoiceController.cs b/Distribution/Features/Invoice/InvoiceController.cs index 5060dcf..71db0df 100644 --- a/Distribution/Features/Invoice/InvoiceController.cs +++ b/Distribution/Features/Invoice/InvoiceController.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Mvc; +using Distribution.Infra.Authorization; using Distribution.Models; namespace Distribution.Features.Invoice; [ApiController] [Route("api/invoices")] -public class InvoiceController(InvoiceService service) : ControllerBase +public class InvoiceController(InvoiceService service, ICurrentUserService currentUser) : ControllerBase { [HttpGet] public async Task GetAll( @@ -26,4 +27,30 @@ public class InvoiceController(InvoiceService service) : ControllerBase var result = await service.GetByIdAsync(id); return result is null ? NotFound() : Ok(result); } + + [HttpPost("{id:guid}/print")] + public async Task Print(Guid id, CancellationToken ct) + { + var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + try + { + var result = await service.PrintAsync(id, userId, ct); + return Ok(result); + } + catch (KeyNotFoundException) { return NotFound(); } + catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } + } + + [HttpPost("{id:guid}/finalize")] + public async Task Finalize(Guid id, [FromBody] FinalizeInvoiceRequest? request, CancellationToken ct) + { + var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + try + { + var result = await service.FinalizeAsync(id, request ?? new FinalizeInvoiceRequest(), userId, ct); + return Ok(result); + } + catch (KeyNotFoundException) { return NotFound(); } + catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } + } } diff --git a/Distribution/Features/Invoice/InvoiceDTOs.cs b/Distribution/Features/Invoice/InvoiceDTOs.cs index dd1b413..018db81 100644 --- a/Distribution/Features/Invoice/InvoiceDTOs.cs +++ b/Distribution/Features/Invoice/InvoiceDTOs.cs @@ -4,7 +4,7 @@ namespace Distribution.Features.Invoice; public record CreateInvoiceRequest(Guid ShopPOId); public record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note = null); -public record FinalizeInvoiceRequest(List Adjustments); +public record FinalizeInvoiceRequest(List? Adjustments = null); public record InvoiceResponse( Guid Id, diff --git a/Distribution/Features/Invoice/InvoiceRepository.cs b/Distribution/Features/Invoice/InvoiceRepository.cs index 72faf5f..addbb88 100644 --- a/Distribution/Features/Invoice/InvoiceRepository.cs +++ b/Distribution/Features/Invoice/InvoiceRepository.cs @@ -9,6 +9,7 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository public async Task GetByIdAsync(Guid id) => await context.Invoices .Include(i => i.ShopPO).ThenInclude(p => p.Store) + .Include(i => i.ShopPO).ThenInclude(p => p.Items.Where(x => x.IsActive)) .Include(i => i.Payments.Where(p => p.IsActive)) .FirstOrDefaultAsync(i => i.Id == id && i.IsActive); diff --git a/Distribution/Features/Invoice/InvoiceService.cs b/Distribution/Features/Invoice/InvoiceService.cs index 51e20f1..f7a8e5c 100644 --- a/Distribution/Features/Invoice/InvoiceService.cs +++ b/Distribution/Features/Invoice/InvoiceService.cs @@ -30,4 +30,61 @@ public class InvoiceService( var items = await repo.GetAllAsync(agentId, routeId, shopId, status, page, pageSize); return new PagedResponse(page, pageSize, total, items.Select(ToResponse).ToList()); } + + 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; } + + return ToResponse(invoice); + } }