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:
@@ -77,6 +77,7 @@ public partial class SeedDataService
|
||||
// A credit-normal account with a debit balance appears in the Trial Balance debit column,
|
||||
// reducing net revenue to match the discounted AR amount that was posted.
|
||||
new Account { AccountNumber = "4950", Name = "Sales Discounts", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = true, IsActive = true, Description = "Contra-revenue for invoice discounts granted to customers", CompanyId = company.Id, CreatedAt = now },
|
||||
new Account { AccountNumber = "4960", Name = "Sales Returns & Allowances", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsSystem = true, IsActive = true, Description = "Contra-revenue for the revenue portion of customer refunds (reversed sales)", CompanyId = company.Id, CreatedAt = now },
|
||||
|
||||
// ── COST OF GOODS SOLD ────────────────────────────────────────────
|
||||
new Account { AccountNumber = "5000", Name = "Cost of Goods Sold", AccountType = AccountType.CostOfGoods, AccountSubType = AccountSubType.CostOfGoodsSold, IsSystem = false, IsActive = true, Description = "Direct cost of services delivered", CompanyId = company.Id, CreatedAt = now },
|
||||
@@ -141,6 +142,30 @@ public partial class SeedDataService
|
||||
added++;
|
||||
}
|
||||
|
||||
// 4960 Sales Returns & Allowances — contra-revenue account that receives the revenue portion
|
||||
// of customer refunds under the "reverse the sale" model (DR Sales Returns + DR Sales Tax / CR Bank).
|
||||
var has4960 = await _context.Set<Account>()
|
||||
.IgnoreQueryFilters()
|
||||
.AnyAsync(a => a.CompanyId == company.Id && a.AccountNumber == "4960" && !a.IsDeleted);
|
||||
|
||||
if (!has4960)
|
||||
{
|
||||
_context.Set<Account>().Add(new Account
|
||||
{
|
||||
AccountNumber = "4960",
|
||||
Name = "Sales Returns & Allowances",
|
||||
AccountType = AccountType.Revenue,
|
||||
AccountSubType = AccountSubType.OtherIncome,
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
Description = "Contra-revenue for the revenue portion of customer refunds (reversed sales)",
|
||||
CompanyId = company.Id,
|
||||
CreatedAt = now
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
added++;
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user