Add invoice print and finalize flow

This commit is contained in:
2026-09-22 16:58:38 +05:30
parent 44e335d542
commit e5dadef863
5 changed files with 135 additions and 4 deletions
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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 }); }
}
}
+1 -1
View File
@@ -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<InvoiceItemAdjustment> Adjustments);
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null);
public record InvoiceResponse(
Guid Id,
@@ -9,6 +9,7 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository
public async Task<Models.Invoice?> 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);
@@ -30,4 +30,61 @@ public class InvoiceService(
var items = await repo.GetAllAsync(agentId, routeId, shopId, status, page, pageSize);
return new PagedResponse<InvoiceResponse>(page, pageSize, total, items.Select(ToResponse).ToList());
}
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);
}
}