Track store credit as a Customer Credits liability (GL)
Audit finding 9b: store-credit refunds and credit memos posted nothing to the GL on issue (only a CreditMemo + Customer.CreditBalance), so outstanding store credit was invisible on the balance sheet and the contra-revenue was recognized only on apply. Introduces a 2350 "Customer Credits" liability so the credit is on the books from issue to apply. Model (chosen): lifecycle-equivalent to before, plus the liability is tracked. - Issue (credit memos, goodwill, and store-credit refunds): DR Sales Discounts (4950) / CR Customer Credits (2350). - Apply: DR Customer Credits / CR AR (was DR Sales Discounts / CR AR). - Void unapplied remainder: DR Customer Credits / CR Sales Discounts. Posting updated in all 8 sites: CreditMemosController Create/Apply/Void and InvoicesController IssueCreditMemo/IssueRefund(store credit)/ApplyCredit/ VoidCreditMemo/CancelRefund. New 2350 account (seed + self-heal). Reporting moved in lockstep so the books still balance: the 4950 contra-revenue shifts from applied -> issued (active memos in full + applied portion of voided), the 2350 liability = unapplied balance on active memos, AR still credited by applications. Updated in FinancialReportService (balance sheet retained earnings, trial balance, P&L) and LedgerService (per-account + prior-balance 2350 section). Verified the balance-sheet identity for active and voided memos by hand; new ledger test covers the 2350 lifecycle. Build clean; 284 unit tests pass. Note: pre-existing quirks left untouched (out of 9b scope) — account 2300 is seeded as "Payroll Liabilities" but resolved as Customer Deposits in code, and LedgerService doesn't recompute 4950 so RecalculateBalances understates it; both predate this change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -560,6 +560,31 @@ public class LedgerServiceTests
|
||||
Assert.DoesNotContain(ar!.Entries, e => e.Source == "Refund");
|
||||
}
|
||||
|
||||
// ── Credit memo: Customer Credits (2350) tracks outstanding store credit, excludes voided ──
|
||||
|
||||
[Fact]
|
||||
public async Task GetAccountLedgerAsync_CustomerCredits2350_TracksOutstandingStoreCredit_ExcludesVoided()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
|
||||
context.Accounts.Add(new Account { Id = 1, CompanyId = 1, AccountNumber = "2350", Name = "Customer Credits", AccountType = AccountType.Liability, AccountSubType = AccountSubType.OtherCurrentLiability, IsActive = true });
|
||||
SeedInvoice(context, id: 99);
|
||||
|
||||
// Active memo: $100 issued, $40 applied → $60 outstanding liability.
|
||||
context.CreditMemos.Add(new CreditMemo { Id = 1, CompanyId = 1, MemoNumber = "CM-1", CustomerId = 1, Amount = 100m, AmountApplied = 40m, IssueDate = InPeriod, Status = CreditMemoStatus.PartiallyApplied });
|
||||
context.CreditMemoApplications.Add(new CreditMemoApplication { Id = 1, CompanyId = 1, CreditMemoId = 1, InvoiceId = 99, AmountApplied = 40m, AppliedDate = InPeriod });
|
||||
|
||||
// Voided memo: must contribute nothing to the liability.
|
||||
context.CreditMemos.Add(new CreditMemo { Id = 2, CompanyId = 1, MemoNumber = "CM-2", CustomerId = 1, Amount = 50m, AmountApplied = 0m, IssueDate = InPeriod, Status = CreditMemoStatus.Voided });
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);
|
||||
|
||||
// CR 100 (issue) + DR 40 (apply) on the active memo; the voided memo is excluded.
|
||||
Assert.Equal(2, ledger!.Entries.Count);
|
||||
Assert.Equal(60m, ledger.ClosingBalance); // credit-normal liability: 100 issued − 40 applied
|
||||
}
|
||||
|
||||
private static LedgerService CreateService(ApplicationDbContext context)
|
||||
=> new LedgerService(context, Mock.Of<ILogger<LedgerService>>());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user