Add manual invoice creation mode with new API & UI flow
CI/CD - ERP Distribution / Build & Deploy Frontend (pull_request) Failing after 13m13s
CI/CD - ERP Distribution / Build & Deploy Backend (pull_request) Failing after 14m28s

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.
This commit is contained in:
Kalana B. Thilakarathna
2026-10-20 13:38:43 +05:30
parent 5707bd4c31
commit 2de9e5e4ad
12 changed files with 344 additions and 5 deletions
@@ -36,6 +36,22 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateInvoiceRequest request, CancellationToken ct)
{
// TEMPORARY: currentUser.UserId is always unset due to the broken auth middleware
// (testbug.md #28) — fall back to the explicitly supplied user until that's fixed.
var userId = currentUser.UserId ?? request.CreatedByUserId;
if (userId is null) return BadRequest(new { error = "CreatedByUserId is required while auth is unavailable" });
try
{
var result = await service.CreateAsync(request, userId.Value, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("{id:guid}/print")]
public async Task<IActionResult> Print(Guid id, [FromBody] PrintInvoiceRequest? request, CancellationToken ct)
{
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Distribution.Features.Invoice;
public record CreateInvoiceRequest(Guid ShopPOId);
public record CreateInvoiceRequest(Guid ShopPOId, Guid? CreatedByUserId = null);
public record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note = null);
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null, Guid? FinalizedByUserId = null);
public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
@@ -1,4 +1,5 @@
using Distribution.Features.Stock;
using Distribution.Features.ShopPO;
using Distribution.Features.Stock;
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
using Distribution.Models;
@@ -7,6 +8,7 @@ namespace Distribution.Features.Invoice;
public class InvoiceService(
IInvoiceRepository repo,
IShopPORepository shopPoRepo,
IUnitOfWork uow,
ICurrentUserService currentUser)
{
@@ -42,6 +44,41 @@ public class InvoiceService(
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)
@@ -30,6 +30,16 @@ public class ShopPOController(ShopPOService service, ICurrentUserService current
return result is null ? NotFound() : Ok(result);
}
[HttpGet("confirmed-without-invoice")]
public async Task<IActionResult> GetConfirmedWithoutInvoice(
[FromQuery] Guid? agentId,
[FromQuery] Guid? routeId,
[FromQuery] Guid? shopId)
{
var result = await service.GetConfirmedWithoutInvoiceAsync(agentId, routeId, shopId);
return Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateShopPORequest request, CancellationToken ct)
{
@@ -55,6 +55,13 @@ public class ShopPOService(
return ToResponse(po, users);
}
public async Task<List<ShopPOResponse>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId)
{
var items = await repo.GetConfirmedWithoutInvoiceAsync(agentId, routeId, shopId);
var users = await repo.GetUserInfoAsync(items.SelectMany(UserIds));
return items.Select(p => ToResponse(p, users)).ToList();
}
public async Task<ShopPOResponse> CreateAsync(CreateShopPORequest request, Guid raisedById, CancellationToken ct = default)
{
if (request.Items.Count == 0)