Make cash refunds reverse the sale instead of re-opening AR
Audit finding 9a: a cash refund posted DR AR (control up) while the customer subledger went down — opposite directions, guaranteeing AR drift. Per the chosen model, a cash refund now *reverses the sale*: DR Sales Returns (revenue portion) + DR Sales Tax Payable (tax portion) / CR Bank, with the customer AR balance left untouched (the invoice stays paid; the sale is contra'd). - New 4960 "Sales Returns & Allowances" contra-revenue account (seed + self-heal). - Tax/revenue split centralized in Core RefundAllocation.Split so the posting and both reporting recomputes compute it identically (a mismatch would unbalance the trial balance). - InvoicesController IssueRefund/CancelRefund: new posting + reversal; no AR/ customer-balance change. - LedgerService (per-account + prior balance): refunds debit Sales Returns + Sales Tax, no longer debit AR; bank credit unchanged. - FinancialReportService: trial balance, balance sheet (retained earnings + tax liability), and P&L (contra-revenue line) all updated to match. Store-credit refunds are excluded everywhere (they post via CreditMemo, not the GL — 9b). Verified: $108 refund (incl. $8 tax) -> bank -100... checks: assets -108 = liabilities -8 + equity -100. New tests cover the split invariant and the ledger postings. Build clean; 283 unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using PowderCoating.Core.Accounting;
|
||||
|
||||
namespace PowderCoating.UnitTests;
|
||||
|
||||
public class RefundAllocationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(108, 8, 108, 100, 8)] // full refund, proportional tax
|
||||
[InlineData(54, 8, 108, 50, 4)] // half refund → half the tax
|
||||
[InlineData(100, 0, 100, 100, 0)] // no tax on the invoice
|
||||
[InlineData(100, 8, 0, 100, 0)] // zero invoice total → all returns, no tax
|
||||
public void Split_AllocatesTaxProportionally_AndAlwaysSumsToRefund(
|
||||
decimal amount, decimal invoiceTax, decimal invoiceTotal, decimal expectedReturns, decimal expectedTax)
|
||||
{
|
||||
var (returns, tax) = RefundAllocation.Split(amount, invoiceTax, invoiceTotal);
|
||||
|
||||
Assert.Equal(expectedReturns, returns);
|
||||
Assert.Equal(expectedTax, tax);
|
||||
// Invariant: the two portions must always reconstruct the refund exactly (no penny lost).
|
||||
Assert.Equal(amount, returns + tax);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user