feat: add scheme discount calculation to invoice processing
This commit is contained in:
@@ -9,6 +9,7 @@ public interface IInvoiceRepository
|
|||||||
Task<List<Models.Invoice>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize);
|
Task<List<Models.Invoice>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize);
|
||||||
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<decimal> GetSchemeDiscountAsync(Guid shopPOId);
|
||||||
Task AddAsync(Models.Invoice invoice);
|
Task AddAsync(Models.Invoice invoice);
|
||||||
Task<Models.Item?> GetItemAsync(Guid itemId);
|
Task<Models.Item?> GetItemAsync(Guid itemId);
|
||||||
Task AddItemsAsync(IEnumerable<InvoiceItem> items);
|
Task AddItemsAsync(IEnumerable<InvoiceItem> items);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public record InvoiceResponse(
|
|||||||
DateTime? FinalizedAt,
|
DateTime? FinalizedAt,
|
||||||
Guid? FinalizedBy,
|
Guid? FinalizedBy,
|
||||||
decimal TotalAmount,
|
decimal TotalAmount,
|
||||||
|
decimal DiscountAmount,
|
||||||
decimal PaidAmount,
|
decimal PaidAmount,
|
||||||
bool IsActive,
|
bool IsActive,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ public class InvoiceRepository(AppDBContext context) : IInvoiceRepository
|
|||||||
.Where(i => i.IsActive && i.PaymentStatus != PaymentStatus.Paid)
|
.Where(i => i.IsActive && i.PaymentStatus != PaymentStatus.Paid)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
public async Task<decimal> GetSchemeDiscountAsync(Guid shopPOId) =>
|
||||||
|
await context.SchemeRedemptions
|
||||||
|
.Where(r => r.FK_ShopPOId == shopPOId)
|
||||||
|
.SumAsync(r => r.DiscountAmount) ?? 0m;
|
||||||
|
|
||||||
public async Task AddAsync(Models.Invoice invoice) =>
|
public async Task AddAsync(Models.Invoice invoice) =>
|
||||||
await context.Invoices.AddAsync(invoice);
|
await context.Invoices.AddAsync(invoice);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class InvoiceService(
|
|||||||
i.Status, i.PaymentStatus, i.CreatedBy,
|
i.Status, i.PaymentStatus, i.CreatedBy,
|
||||||
i.PrintedAt, i.PrintedBy,
|
i.PrintedAt, i.PrintedBy,
|
||||||
i.FinalizedAt, i.FinalizedBy,
|
i.FinalizedAt, i.FinalizedBy,
|
||||||
i.TotalAmount,
|
i.TotalAmount, i.DiscountAmount,
|
||||||
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 =>
|
i.Items.Where(x => x.IsActive).Select(x =>
|
||||||
@@ -30,6 +30,15 @@ public class InvoiceService(
|
|||||||
private static decimal BillableTotal(IEnumerable<InvoiceItem> items) =>
|
private static decimal BillableTotal(IEnumerable<InvoiceItem> items) =>
|
||||||
items.Where(i => i.IsActive && !i.IsRemoved).Sum(i => i.Quantity * i.UnitPrice);
|
items.Where(i => i.IsActive && !i.IsRemoved).Sum(i => i.Quantity * i.UnitPrice);
|
||||||
|
|
||||||
|
// TotalAmount = billable lines - scheme discount redeemed on the PO. Re-derived whenever lines change
|
||||||
|
// and at finalize, so a scheme applied after the invoice was drafted is still picked up before it locks.
|
||||||
|
private async Task ApplyTotalsAsync(Models.Invoice invoice, IEnumerable<InvoiceItem> items)
|
||||||
|
{
|
||||||
|
var billable = BillableTotal(items);
|
||||||
|
invoice.DiscountAmount = Math.Min(await repo.GetSchemeDiscountAsync(invoice.FK_ShopPOId), billable);
|
||||||
|
invoice.TotalAmount = billable - invoice.DiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
// Immutable copy of the invoice lines as they stand right now.
|
// 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) =>
|
public static InvoiceVersion BuildVersion(Models.Invoice invoice, IEnumerable<InvoiceItem> items, int versionNumber, InvoiceVersionStage stage, Guid userId) =>
|
||||||
new()
|
new()
|
||||||
@@ -115,15 +124,15 @@ public class InvoiceService(
|
|||||||
FK_ShopPOId = po.Id,
|
FK_ShopPOId = po.Id,
|
||||||
Status = InvoiceStatus.Draft,
|
Status = InvoiceStatus.Draft,
|
||||||
PaymentStatus = PaymentStatus.Pending,
|
PaymentStatus = PaymentStatus.Pending,
|
||||||
CreatedBy = createdBy,
|
CreatedBy = createdBy
|
||||||
TotalAmount = po.TotalAmount
|
|
||||||
};
|
};
|
||||||
|
var lines = BuildItemsFromShopPO(po, invoice.Id);
|
||||||
|
await ApplyTotalsAsync(invoice, lines);
|
||||||
|
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await repo.AddAsync(invoice);
|
await repo.AddAsync(invoice);
|
||||||
var lines = BuildItemsFromShopPO(po, invoice.Id);
|
|
||||||
await repo.AddItemsAsync(lines);
|
await repo.AddItemsAsync(lines);
|
||||||
await repo.AddVersionAsync(BuildVersion(invoice, lines, 1, InvoiceVersionStage.Created, createdBy));
|
await repo.AddVersionAsync(BuildVersion(invoice, lines, 1, InvoiceVersionStage.Created, createdBy));
|
||||||
po.InvoiceId = invoice.Id;
|
po.InvoiceId = invoice.Id;
|
||||||
@@ -192,7 +201,7 @@ public class InvoiceService(
|
|||||||
throw new InvalidOperationException("Each edit needs an invoiceItemId (existing line) or an itemId (new line)");
|
throw new InvalidOperationException("Each edit needs an invoiceItemId (existing line) or an itemId (new line)");
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.TotalAmount = BillableTotal(invoice.Items.Concat(newLines));
|
await ApplyTotalsAsync(invoice, invoice.Items.Concat(newLines));
|
||||||
|
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
try
|
try
|
||||||
@@ -223,6 +232,10 @@ public class InvoiceService(
|
|||||||
if (invoice.Status != InvoiceStatus.Draft)
|
if (invoice.Status != InvoiceStatus.Draft)
|
||||||
throw new InvalidOperationException($"Cannot print an invoice in {invoice.Status} status");
|
throw new InvalidOperationException($"Cannot print an invoice in {invoice.Status} status");
|
||||||
|
|
||||||
|
// Printing fixes the amount the customer sees, so pick up any scheme discount applied since the last edit.
|
||||||
|
var lines = invoice.Items.Where(i => i.IsActive).ToList();
|
||||||
|
if (lines.Count > 0) await ApplyTotalsAsync(invoice, lines);
|
||||||
|
|
||||||
invoice.Status = InvoiceStatus.Printed;
|
invoice.Status = InvoiceStatus.Printed;
|
||||||
invoice.PrintedAt = DateTime.Now;
|
invoice.PrintedAt = DateTime.Now;
|
||||||
invoice.PrintedBy = printedBy;
|
invoice.PrintedBy = printedBy;
|
||||||
@@ -265,7 +278,7 @@ public class InvoiceService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (items.Count > 0)
|
if (items.Count > 0)
|
||||||
invoice.TotalAmount = BillableTotal(items);
|
await ApplyTotalsAsync(invoice, items);
|
||||||
|
|
||||||
invoice.Status = InvoiceStatus.Finalized;
|
invoice.Status = InvoiceStatus.Finalized;
|
||||||
invoice.FinalizedAt = DateTime.Now;
|
invoice.FinalizedAt = DateTime.Now;
|
||||||
|
|||||||
Reference in New Issue
Block a user