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,29 @@
|
||||
namespace PowderCoating.Core.Accounting;
|
||||
|
||||
/// <summary>
|
||||
/// Single source of truth for splitting a customer refund into its revenue (returns) portion and
|
||||
/// its sales-tax portion, under the "reverse the sale" model. A refund of a paid invoice reverses
|
||||
/// the original sale: the revenue portion is debited to Sales Returns (contra-revenue) and the tax
|
||||
/// portion is debited to Sales Tax Payable (reducing the liability), with cash credited out.
|
||||
///
|
||||
/// The split is proportional to the parent invoice's tax ratio so a partial refund reverses the
|
||||
/// right amount of tax. Centralised here so the posting (<c>InvoicesController</c>) and the two
|
||||
/// reporting recomputes (<c>LedgerService</c>, <c>FinancialReportService</c>) always agree — if they
|
||||
/// computed it independently the trial balance could drift.
|
||||
/// </summary>
|
||||
public static class RefundAllocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Splits <paramref name="refundAmount"/> (tax-inclusive) into (returnsPortion, taxPortion) using
|
||||
/// the parent invoice's tax ratio. When the invoice has no total or no tax, the whole refund is
|
||||
/// the returns portion and the tax portion is zero.
|
||||
/// </summary>
|
||||
public static (decimal ReturnsPortion, decimal TaxPortion) Split(
|
||||
decimal refundAmount, decimal invoiceTaxAmount, decimal invoiceTotal)
|
||||
{
|
||||
var taxPortion = invoiceTotal > 0m && invoiceTaxAmount > 0m
|
||||
? Math.Round(refundAmount * invoiceTaxAmount / invoiceTotal, 2, MidpointRounding.AwayFromZero)
|
||||
: 0m;
|
||||
return (refundAmount - taxPortion, taxPortion);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user