Invoice handling with versioning and item updates
This commit is contained in:
@@ -10,5 +10,10 @@ public interface IInvoiceRepository
|
|||||||
Task<int> GetCountAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus);
|
Task<int> GetCountAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus);
|
||||||
Task<List<Models.Invoice>> GetOutstandingAsync();
|
Task<List<Models.Invoice>> GetOutstandingAsync();
|
||||||
Task AddAsync(Models.Invoice invoice);
|
Task AddAsync(Models.Invoice invoice);
|
||||||
|
Task<Models.Item?> GetItemAsync(Guid itemId);
|
||||||
|
Task AddItemsAsync(IEnumerable<InvoiceItem> items);
|
||||||
|
Task AddVersionAsync(InvoiceVersion version);
|
||||||
|
Task<int> GetLatestVersionNumberAsync(Guid invoiceId);
|
||||||
|
Task<List<InvoiceVersion>> GetVersionsAsync(Guid invoiceId);
|
||||||
void Update(Models.Invoice invoice);
|
void Update(Models.Invoice invoice);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,29 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre
|
|||||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id:guid}/versions")]
|
||||||
|
public async Task<IActionResult> GetVersions(Guid id)
|
||||||
|
{
|
||||||
|
var result = await service.GetVersionsAsync(id);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id:guid}/items")]
|
||||||
|
public async Task<IActionResult> UpdateItems(Guid id, [FromBody] UpdateInvoiceItemsRequest 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.EditedByUserId;
|
||||||
|
if (userId is null) return BadRequest(new { error = "EditedByUserId is required while auth is unavailable" });
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.UpdateItemsAsync(id, request, userId.Value, ct);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException) { return NotFound(); }
|
||||||
|
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/print")]
|
[HttpPost("{id:guid}/print")]
|
||||||
public async Task<IActionResult> Print(Guid id, [FromBody] PrintInvoiceRequest? request, CancellationToken ct)
|
public async Task<IActionResult> Print(Guid id, [FromBody] PrintInvoiceRequest? request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,10 +3,35 @@
|
|||||||
namespace Distribution.Features.Invoice;
|
namespace Distribution.Features.Invoice;
|
||||||
|
|
||||||
public record CreateInvoiceRequest(Guid ShopPOId, Guid? CreatedByUserId = null);
|
public record CreateInvoiceRequest(Guid ShopPOId, Guid? CreatedByUserId = null);
|
||||||
public record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note = null);
|
public record InvoiceItemAdjustment(Guid InvoiceItemId, int AdjustedQuantity, bool IsRemoved = false, string? Note = null);
|
||||||
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null, Guid? FinalizedByUserId = null);
|
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null, Guid? FinalizedByUserId = null);
|
||||||
public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
|
public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
|
||||||
|
|
||||||
|
// InvoiceItemId set = edit/remove/restore an existing line; ItemId set (InvoiceItemId null) = add a new line.
|
||||||
|
public record InvoiceItemEdit(Guid? InvoiceItemId, Guid? ItemId, int Quantity, decimal? UnitPrice = null, bool IsRemoved = false);
|
||||||
|
public record UpdateInvoiceItemsRequest(List<InvoiceItemEdit> Items, Guid? EditedByUserId = null);
|
||||||
|
|
||||||
|
public record InvoiceVersionItemResponse(Guid? ShopPOItemId, Guid ItemId, string ItemName, int Quantity, decimal UnitPrice, bool IsRemoved);
|
||||||
|
|
||||||
|
public record InvoiceVersionResponse(
|
||||||
|
Guid Id,
|
||||||
|
int VersionNumber,
|
||||||
|
InvoiceVersionStage Stage,
|
||||||
|
decimal TotalAmount,
|
||||||
|
Guid CreatedBy,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
List<InvoiceVersionItemResponse> Items);
|
||||||
|
|
||||||
|
public record InvoiceItemResponse(
|
||||||
|
Guid Id,
|
||||||
|
Guid? ShopPOItemId,
|
||||||
|
Guid ItemId,
|
||||||
|
string ItemName,
|
||||||
|
int? OrderedQuantity,
|
||||||
|
int Quantity,
|
||||||
|
decimal UnitPrice,
|
||||||
|
bool IsRemoved);
|
||||||
|
|
||||||
public record InvoiceResponse(
|
public record InvoiceResponse(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid ShopPOId,
|
Guid ShopPOId,
|
||||||
@@ -22,6 +47,7 @@ public record InvoiceResponse(
|
|||||||
decimal TotalAmount,
|
decimal TotalAmount,
|
||||||
decimal PaidAmount,
|
decimal PaidAmount,
|
||||||
bool IsActive,
|
bool IsActive,
|
||||||
DateTime CreatedAt);
|
DateTime CreatedAt,
|
||||||
|
List<InvoiceItemResponse> Items);
|
||||||
|
|
||||||
public record OutstandingPaymentsResponse(int InvoiceCount, decimal OutstandingAmount);
|
public record OutstandingPaymentsResponse(int InvoiceCount, decimal OutstandingAmount);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository
|
|||||||
await context.Invoices
|
await context.Invoices
|
||||||
.Include(i => i.ShopPO).ThenInclude(p => p.Store)
|
.Include(i => i.ShopPO).ThenInclude(p => p.Store)
|
||||||
.Include(i => i.ShopPO).ThenInclude(p => p.Items.Where(x => x.IsActive))
|
.Include(i => i.ShopPO).ThenInclude(p => p.Items.Where(x => x.IsActive))
|
||||||
|
.Include(i => i.Items.Where(x => x.IsActive)).ThenInclude(x => x.Item)
|
||||||
|
.Include(i => i.Items.Where(x => x.IsActive)).ThenInclude(x => x.ShopPOItem)
|
||||||
.Include(i => i.Payments.Where(p => p.IsActive))
|
.Include(i => i.Payments.Where(p => p.IsActive))
|
||||||
.FirstOrDefaultAsync(i => i.Id == id && i.IsActive);
|
.FirstOrDefaultAsync(i => i.Id == id && i.IsActive);
|
||||||
|
|
||||||
@@ -52,6 +54,27 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository
|
|||||||
public async Task AddAsync(Models.Invoice invoice) =>
|
public async Task AddAsync(Models.Invoice invoice) =>
|
||||||
await context.Invoices.AddAsync(invoice);
|
await context.Invoices.AddAsync(invoice);
|
||||||
|
|
||||||
|
public async Task<Models.Item?> GetItemAsync(Guid itemId) =>
|
||||||
|
await context.Items.FirstOrDefaultAsync(i => i.Id == itemId && i.IsActive);
|
||||||
|
|
||||||
|
public async Task AddItemsAsync(IEnumerable<InvoiceItem> items) =>
|
||||||
|
await context.InvoiceItems.AddRangeAsync(items);
|
||||||
|
|
||||||
|
public async Task AddVersionAsync(InvoiceVersion version) =>
|
||||||
|
await context.InvoiceVersions.AddAsync(version);
|
||||||
|
|
||||||
|
public async Task<int> GetLatestVersionNumberAsync(Guid invoiceId) =>
|
||||||
|
await context.InvoiceVersions
|
||||||
|
.Where(v => v.FK_InvoiceId == invoiceId)
|
||||||
|
.MaxAsync(v => (int?)v.VersionNumber) ?? 0;
|
||||||
|
|
||||||
|
public async Task<List<InvoiceVersion>> GetVersionsAsync(Guid invoiceId) =>
|
||||||
|
await context.InvoiceVersions
|
||||||
|
.Include(v => v.Items).ThenInclude(i => i.Item)
|
||||||
|
.Where(v => v.FK_InvoiceId == invoiceId && v.IsActive)
|
||||||
|
.OrderBy(v => v.VersionNumber)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
public void Update(Models.Invoice invoice) =>
|
public void Update(Models.Invoice invoice) =>
|
||||||
context.Invoices.Update(invoice);
|
context.Invoices.Update(invoice);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,61 @@ public class InvoiceService(
|
|||||||
i.FinalizedAt, i.FinalizedBy,
|
i.FinalizedAt, i.FinalizedBy,
|
||||||
i.TotalAmount,
|
i.TotalAmount,
|
||||||
i.Payments?.Where(p => p.IsActive).Sum(p => p.Amount) ?? 0m,
|
i.Payments?.Where(p => p.IsActive).Sum(p => p.Amount) ?? 0m,
|
||||||
i.IsActive, i.CreatedAt);
|
i.IsActive, i.CreatedAt,
|
||||||
|
i.Items.Where(x => x.IsActive).Select(x =>
|
||||||
|
new InvoiceItemResponse(x.Id, x.FK_ShopPOItemId, x.FK_ItemId,
|
||||||
|
x.Item?.Name ?? string.Empty, x.ShopPOItem?.Quantity,
|
||||||
|
x.Quantity, x.UnitPrice, x.IsRemoved)).ToList());
|
||||||
|
|
||||||
|
private static decimal BillableTotal(IEnumerable<InvoiceItem> items) =>
|
||||||
|
items.Where(i => i.IsActive && !i.IsRemoved).Sum(i => i.Quantity * i.UnitPrice);
|
||||||
|
|
||||||
|
// Immutable copy of the invoice lines as they stand right now.
|
||||||
|
public static InvoiceVersion BuildVersion(Models.Invoice invoice, IEnumerable<InvoiceItem> items, int versionNumber, InvoiceVersionStage stage, Guid userId) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FK_InvoiceId = invoice.Id,
|
||||||
|
VersionNumber = versionNumber,
|
||||||
|
Stage = stage,
|
||||||
|
TotalAmount = invoice.TotalAmount,
|
||||||
|
CreatedBy = userId,
|
||||||
|
Items = items.Select(i => new InvoiceVersionItem
|
||||||
|
{
|
||||||
|
FK_ShopPOItemId = i.FK_ShopPOItemId,
|
||||||
|
FK_ItemId = i.FK_ItemId,
|
||||||
|
Quantity = i.Quantity,
|
||||||
|
UnitPrice = i.UnitPrice,
|
||||||
|
IsRemoved = i.IsRemoved
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task<List<InvoiceVersionResponse>?> GetVersionsAsync(Guid id)
|
||||||
|
{
|
||||||
|
if (await repo.GetByIdAsync(id) is null) return null;
|
||||||
|
var versions = await repo.GetVersionsAsync(id);
|
||||||
|
return versions.Select(v => new InvoiceVersionResponse(
|
||||||
|
v.Id, v.VersionNumber, v.Stage, v.TotalAmount, v.CreatedBy, v.CreatedAt,
|
||||||
|
v.Items.Select(i => new InvoiceVersionItemResponse(
|
||||||
|
i.FK_ShopPOItemId, i.FK_ItemId, i.Item?.Name ?? string.Empty, i.Quantity, i.UnitPrice, i.IsRemoved)).ToList())).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddVersionAsync(Models.Invoice invoice, InvoiceVersionStage stage, Guid userId, IEnumerable<InvoiceItem>? addedLines = null)
|
||||||
|
{
|
||||||
|
var next = await repo.GetLatestVersionNumberAsync(invoice.Id) + 1;
|
||||||
|
// Added lines may or may not be in invoice.Items yet, so merge by reference.
|
||||||
|
var lines = invoice.Items.Concat(addedLines ?? []).Where(i => i.IsActive).Distinct();
|
||||||
|
await repo.AddVersionAsync(BuildVersion(invoice, lines, next, stage, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<InvoiceItem> BuildItemsFromShopPO(Models.ShopPO po, Guid invoiceId) =>
|
||||||
|
po.Items.Where(i => i.IsActive).Select(i => new InvoiceItem
|
||||||
|
{
|
||||||
|
FK_InvoiceId = invoiceId,
|
||||||
|
FK_ShopPOItemId = i.Id,
|
||||||
|
FK_ItemId = i.FK_ItemId,
|
||||||
|
Quantity = i.Quantity,
|
||||||
|
UnitPrice = i.UnitPrice
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
public async Task<InvoiceResponse?> GetByIdAsync(Guid id)
|
public async Task<InvoiceResponse?> GetByIdAsync(Guid id)
|
||||||
{
|
{
|
||||||
@@ -69,6 +123,9 @@ public class InvoiceService(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await repo.AddAsync(invoice);
|
await repo.AddAsync(invoice);
|
||||||
|
var lines = BuildItemsFromShopPO(po, invoice.Id);
|
||||||
|
await repo.AddItemsAsync(lines);
|
||||||
|
await repo.AddVersionAsync(BuildVersion(invoice, lines, 1, InvoiceVersionStage.Created, createdBy));
|
||||||
po.InvoiceId = invoice.Id;
|
po.InvoiceId = invoice.Id;
|
||||||
shopPoRepo.Update(po);
|
shopPoRepo.Update(po);
|
||||||
await uow.CommitAsync(ct);
|
await uow.CommitAsync(ct);
|
||||||
@@ -79,6 +136,85 @@ public class InvoiceService(
|
|||||||
return ToResponse(saved!);
|
return ToResponse(saved!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<InvoiceResponse> UpdateItemsAsync(Guid id, UpdateInvoiceItemsRequest request, Guid editedBy, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var invoice = await repo.GetByIdAsync(id)
|
||||||
|
?? throw new KeyNotFoundException("Invoice not found");
|
||||||
|
|
||||||
|
if (invoice.Status != InvoiceStatus.Draft)
|
||||||
|
throw new InvalidOperationException($"Items can only be edited while the invoice is Draft (current: {invoice.Status})");
|
||||||
|
|
||||||
|
var newLines = new List<InvoiceItem>();
|
||||||
|
|
||||||
|
foreach (var edit in request.Items)
|
||||||
|
{
|
||||||
|
if (edit.UnitPrice is < 0)
|
||||||
|
throw new InvalidOperationException("Unit price cannot be negative");
|
||||||
|
|
||||||
|
if (edit.InvoiceItemId is Guid lineId)
|
||||||
|
{
|
||||||
|
// Existing line: edit quantity/price, or strike it (IsRemoved) / restore it.
|
||||||
|
var item = invoice.Items.FirstOrDefault(i => i.Id == lineId && i.IsActive)
|
||||||
|
?? throw new InvalidOperationException($"Invoice item {lineId} not found on this invoice");
|
||||||
|
|
||||||
|
item.IsRemoved = edit.IsRemoved;
|
||||||
|
if (!edit.IsRemoved)
|
||||||
|
{
|
||||||
|
ValidateQuantity(item, edit.Quantity);
|
||||||
|
item.Quantity = edit.Quantity;
|
||||||
|
}
|
||||||
|
if (edit.UnitPrice is decimal price) item.UnitPrice = price;
|
||||||
|
}
|
||||||
|
else if (edit.ItemId is Guid itemId)
|
||||||
|
{
|
||||||
|
// New line for an item that was not on the PO.
|
||||||
|
if (edit.IsRemoved) continue;
|
||||||
|
if (edit.Quantity <= 0)
|
||||||
|
throw new InvalidOperationException("Quantity for an added item must be greater than zero");
|
||||||
|
if (edit.UnitPrice is null)
|
||||||
|
throw new InvalidOperationException("Unit price is required for an added item");
|
||||||
|
if (invoice.Items.Concat(newLines).Any(i => i.IsActive && !i.IsRemoved && i.FK_ItemId == itemId))
|
||||||
|
throw new InvalidOperationException("This item is already on the invoice — edit its existing line instead");
|
||||||
|
|
||||||
|
var catalogItem = await repo.GetItemAsync(itemId)
|
||||||
|
?? throw new InvalidOperationException($"Item {itemId} not found");
|
||||||
|
|
||||||
|
newLines.Add(new InvoiceItem
|
||||||
|
{
|
||||||
|
FK_InvoiceId = invoice.Id,
|
||||||
|
FK_ItemId = catalogItem.Id,
|
||||||
|
Item = catalogItem,
|
||||||
|
Quantity = edit.Quantity,
|
||||||
|
UnitPrice = edit.UnitPrice.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException("Each edit needs an invoiceItemId (existing line) or an itemId (new line)");
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.TotalAmount = BillableTotal(invoice.Items.Concat(newLines));
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
repo.Update(invoice);
|
||||||
|
if (newLines.Count > 0) await repo.AddItemsAsync(newLines);
|
||||||
|
await AddVersionAsync(invoice, InvoiceVersionStage.DraftEdited, editedBy, newLines);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
return ToResponse((await repo.GetByIdAsync(id))!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateQuantity(InvoiceItem item, int quantity)
|
||||||
|
{
|
||||||
|
var ordered = item.ShopPOItem?.Quantity ?? int.MaxValue;
|
||||||
|
if (quantity < 0 || quantity > ordered)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Quantity for item {item.Id} must be between 0 and {ordered}");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<InvoiceResponse> PrintAsync(Guid id, Guid printedBy, CancellationToken ct = default)
|
public async Task<InvoiceResponse> PrintAsync(Guid id, Guid printedBy, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var invoice = await repo.GetByIdAsync(id)
|
var invoice = await repo.GetByIdAsync(id)
|
||||||
@@ -92,7 +228,12 @@ public class InvoiceService(
|
|||||||
invoice.PrintedBy = printedBy;
|
invoice.PrintedBy = printedBy;
|
||||||
|
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
try { repo.Update(invoice); await uow.CommitAsync(ct); }
|
try
|
||||||
|
{
|
||||||
|
repo.Update(invoice);
|
||||||
|
await AddVersionAsync(invoice, InvoiceVersionStage.Printed, printedBy);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
catch { await uow.RollbackAsync(ct); throw; }
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
return ToResponse(invoice);
|
return ToResponse(invoice);
|
||||||
@@ -106,31 +247,37 @@ public class InvoiceService(
|
|||||||
if (invoice.Status != InvoiceStatus.Printed)
|
if (invoice.Status != InvoiceStatus.Printed)
|
||||||
throw new InvalidOperationException($"Cannot finalize an invoice in {invoice.Status} status");
|
throw new InvalidOperationException($"Cannot finalize an invoice in {invoice.Status} status");
|
||||||
|
|
||||||
|
var items = invoice.Items.Where(i => i.IsActive).ToList();
|
||||||
var adjustments = request.Adjustments ?? [];
|
var adjustments = request.Adjustments ?? [];
|
||||||
if (adjustments.Count > 0)
|
|
||||||
|
// Agent's handwritten edits from the printed copy are written onto the invoice lines.
|
||||||
|
foreach (var adjustment in adjustments)
|
||||||
{
|
{
|
||||||
var poItems = invoice.ShopPO.Items.Where(i => i.IsActive).ToList();
|
var item = items.FirstOrDefault(i => i.Id == adjustment.InvoiceItemId)
|
||||||
decimal total = 0;
|
?? throw new InvalidOperationException($"Invoice item {adjustment.InvoiceItemId} is not on this invoice");
|
||||||
foreach (var item in poItems)
|
|
||||||
|
item.IsRemoved = adjustment.IsRemoved;
|
||||||
|
if (!adjustment.IsRemoved)
|
||||||
{
|
{
|
||||||
var adjustment = adjustments.FirstOrDefault(a => a.ShopPOItemId == item.Id);
|
ValidateQuantity(item, adjustment.AdjustedQuantity);
|
||||||
var billedQuantity = adjustment is not null ? adjustment.AdjustedQuantity : item.Quantity;
|
item.Quantity = adjustment.AdjustedQuantity;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (items.Count > 0)
|
||||||
|
invoice.TotalAmount = BillableTotal(items);
|
||||||
|
|
||||||
invoice.Status = InvoiceStatus.Finalized;
|
invoice.Status = InvoiceStatus.Finalized;
|
||||||
invoice.FinalizedAt = DateTime.Now;
|
invoice.FinalizedAt = DateTime.Now;
|
||||||
invoice.FinalizedBy = finalizedBy;
|
invoice.FinalizedBy = finalizedBy;
|
||||||
|
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
try { repo.Update(invoice); await uow.CommitAsync(ct); }
|
try
|
||||||
|
{
|
||||||
|
repo.Update(invoice);
|
||||||
|
await AddVersionAsync(invoice, InvoiceVersionStage.Finalized, finalizedBy);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
catch { await uow.RollbackAsync(ct); throw; }
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
return ToResponse(invoice);
|
return ToResponse(invoice);
|
||||||
|
|||||||
Reference in New Issue
Block a user