Fix Customer Deposits account mislabel and Sales Discounts recalc (audit O1, O2)
O1: account 2300 has always been used by the deposit GL code as the Customer
Deposits liability (resolved by number), but it was seeded/named "Payroll
Liabilities" for tenants the AccountingDepositsGL migration's NOT EXISTS guard
skipped — so the liability was mislabeled on the balance sheet. Rename 2300 to
"Customer Deposits" (IsSystem) and move payroll to a new 2400 account:
- both seed paths (SeedDataService.Accounts, SeedData)
- EnsureSystemAccountsAsync self-heal (renames only where still default-named,
preserving user renames; ensures 2400 exists)
- migration RenameDepositsAccountAddPayroll for existing tenants
Account number 2300 is unchanged, so the deposit posting code needs no changes.
O2: LedgerService never recomputed 4950 Sales Discounts, so "Recalculate
Balances" wiped it to JE-only and the Balance Reconciliation report showed false
drift. Add a 4950 section to GetAccountLedgerAsync and ComputePriorBalanceAsync
that reproduces the actual postings (invoice discounts DR + credit-memo issuance
DR, less the unapplied remainder of voided memos CR), matching AccountBalanceService.
Adds a LedgerService regression test for 4950. Documents both fixes plus the
remaining open findings (O3, O4) in docs/ACCOUNTING_AUDIT.md so the audit is no
longer lost. Build clean; 291 unit tests pass; migration applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -585,6 +585,40 @@ public class LedgerServiceTests
|
||||
Assert.Equal(60m, ledger.ClosingBalance); // credit-normal liability: 100 issued − 40 applied
|
||||
}
|
||||
|
||||
// ── Sales Discounts (4950): recompute mirrors the actual postings so a balance ──
|
||||
// ── recalc reproduces the stored balance instead of wiping it to JE-only (O2). ──
|
||||
|
||||
[Fact]
|
||||
public async Task GetAccountLedgerAsync_SalesDiscounts4950_IncludesInvoiceDiscountsAndCreditMemoContraRevenue()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
|
||||
context.Accounts.Add(new Account { Id = 1, CompanyId = 1, AccountNumber = "4950", Name = "Sales Discounts", AccountType = AccountType.Revenue, AccountSubType = AccountSubType.OtherIncome, IsActive = true });
|
||||
|
||||
// Invoice with a $30 discount → DR 4950 at invoice date.
|
||||
context.Invoices.Add(new Invoice
|
||||
{
|
||||
Id = 99, CompanyId = 1, InvoiceNumber = "INV-0099", CustomerId = 1,
|
||||
Status = InvoiceStatus.Sent, InvoiceDate = InPeriod, Total = 270m, DiscountAmount = 30m
|
||||
});
|
||||
|
||||
// Active memo $100 → DR 4950 = full amount at issue (the applied portion stays as contra-revenue).
|
||||
context.CreditMemos.Add(new CreditMemo { Id = 1, CompanyId = 1, MemoNumber = "CM-1", CustomerId = 1, Amount = 100m, AmountApplied = 40m, IssueDate = InPeriod, Status = CreditMemoStatus.PartiallyApplied });
|
||||
|
||||
// Voided memo $50 with $20 applied → DR 50 at issue, CR (50−20)=30 unapplied remainder at void.
|
||||
context.CreditMemos.Add(new CreditMemo { Id = 2, CompanyId = 1, MemoNumber = "CM-2", CustomerId = 1, Amount = 50m, AmountApplied = 20m, IssueDate = InPeriod, Status = CreditMemoStatus.Voided, UpdatedAt = InPeriod });
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var ledger = await CreateService(context).GetAccountLedgerAsync(1, PeriodStart, PeriodEnd);
|
||||
|
||||
// Entries: invoice discount + two memo issuances + one void reversal.
|
||||
Assert.Equal(4, ledger!.Entries.Count);
|
||||
Assert.Equal(180m, ledger.PeriodDebits); // 30 + 100 + 50
|
||||
Assert.Equal(30m, ledger.PeriodCredits); // voided unapplied remainder
|
||||
// Credit-normal contra-revenue with a net debit balance → −150 (shows in the TB debit column).
|
||||
Assert.Equal(-150m, ledger.ClosingBalance);
|
||||
}
|
||||
|
||||
private static LedgerService CreateService(ApplicationDbContext context)
|
||||
=> new LedgerService(context, Mock.Of<ILogger<LedgerService>>());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user