Document the JournalEntry single-source ledger refactor plan
Captures the phased plan to make JournalEntry lines the single source of truth for all GL balances/reports, retiring the parallel re-derivation in LedgerService and FinancialReportService. Resolves the O2/O6/O7/O8 bug class structurally and folds in O9 (inventory capitalization) at Phase 5. Proposed only — not started. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
# Accounting Ledger Refactor — Single Source of Truth
|
||||||
|
|
||||||
|
> Status: **PROPOSED / not started.** This is a captured plan, not in-progress work.
|
||||||
|
> Companion to `docs/ACCOUNTING_AUDIT.md`. Resolves the O2/O6/O7/O8 bug *class* and the open
|
||||||
|
> finding **O9** (inventory capitalization). Do this when there's runway + a staging environment —
|
||||||
|
> it is not an emergency; after audit fixes O1–O8 the current books are correct.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Today every financial event's GL effect is encoded in **three** places:
|
||||||
|
|
||||||
|
1. **`Account.CurrentBalance`** — updated live at each posting site via
|
||||||
|
`AccountBalanceService.DebitAsync/CreditAsync`.
|
||||||
|
2. **`LedgerService`** — re-derives each account's balance by reading *source documents* (Payments,
|
||||||
|
Invoices, Bills, InventoryTransactions, GiftCertificates, CreditMemos, …). Drives
|
||||||
|
`RecalculateAllAsync` and the Balance Reconciliation report.
|
||||||
|
3. **`FinancialReportService`** — *independently* re-derives P&L, Balance Sheet, and Trial Balance from
|
||||||
|
the same documents, with its own per-report logic (largely duplicated across the three reports).
|
||||||
|
|
||||||
|
Adding a transaction type means hand-mirroring its debit and credit into all three engines, in the right
|
||||||
|
direction, in every report method. Missing one spot produced every finding in the audit:
|
||||||
|
|
||||||
|
| Finding | Missed mirror |
|
||||||
|
|---|---|
|
||||||
|
| O2 | 4950 Sales Discounts absent from `LedgerService` → recalc wiped it |
|
||||||
|
| O6 | inventory-consumption COGS absent from both recompute engines |
|
||||||
|
| O7 | GC redemption's AR credit absent → **Trial Balance did not balance** |
|
||||||
|
| O8 | written-off payments filtered inconsistently between the two engines |
|
||||||
|
|
||||||
|
These are one design flaw with four faces: **reports re-interpret documents instead of reading a ledger.**
|
||||||
|
|
||||||
|
## Target
|
||||||
|
|
||||||
|
The journal becomes the only representation of GL effect. The tables already exist
|
||||||
|
(`JournalEntry` + `JournalEntryLine`, with a `Posted` status).
|
||||||
|
|
||||||
|
- **Every** financial event posts a balanced `JournalEntry` (DR == CR enforced once, centrally).
|
||||||
|
- `Account.CurrentBalance`, Trial Balance, Balance Sheet, P&L, and the recompute all derive from
|
||||||
|
**`JournalEntryLine` only** (plus each account's opening balance).
|
||||||
|
- Invoices/Payments/Bills remain *operational* records; they stop carrying implicit GL meaning that every
|
||||||
|
report must re-decode.
|
||||||
|
|
||||||
|
Result: `LedgerService.GetAccountLedgerAsync` (~800 lines of per-document sections) collapses to
|
||||||
|
"sum the JE lines for this account in range." Trial Balance / Balance Sheet become straight per-account JE
|
||||||
|
aggregations. The special cases — `gcRedeemed`, `cmApplied`, refund splits, consumption COGS, discount
|
||||||
|
contra-revenue — all disappear because they are simply journal lines.
|
||||||
|
|
||||||
|
## What changes in the code
|
||||||
|
|
||||||
|
1. **One posting service:** `PostAsync(date, description, lines[])` — validates balanced, assigns the
|
||||||
|
`JE-YYMM-####` number, marks `Posted`, updates `CurrentBalance` from the lines. `AccountBalanceService`
|
||||||
|
becomes a thin wrapper over it.
|
||||||
|
2. **Convert every posting site** to build JE lines instead of ad-hoc Debit/Credit pairs: invoice create,
|
||||||
|
payment, deposit record/apply, refund, credit memo issue/apply/void, GC issue/redeem/void, bill, bill
|
||||||
|
payment, vendor credit, inventory consumption, write-off, sales-tax remittance, year-end close,
|
||||||
|
depreciation, Stripe. Many already call `DebitAsync`; mechanical but touches **every accounting
|
||||||
|
controller**. `InvoicesController.WriteOff` already posts a JE — copy that model everywhere.
|
||||||
|
3. **Edits/voids become reversing entries** (post an opposite JE), never mutate in place.
|
||||||
|
4. **Gut the re-derivation:** `LedgerService` → JE-only; `FinancialReportService` TB/BS/P&L → JE-only.
|
||||||
|
AR/AP **aging** and customer/vendor statements stay document-driven (they answer "which documents are
|
||||||
|
open," not "what is the GL balance"). The AR control account then auto-reconciles to the subledger
|
||||||
|
because both come from the same postings.
|
||||||
|
|
||||||
|
## Migration of historical data
|
||||||
|
|
||||||
|
- **(a) Replay** — re-run every historical document through the new posting logic to emit JEs. Most
|
||||||
|
faithful; must handle edits/voids in chronological order. Complex, higher risk.
|
||||||
|
- **(b) Conversion balances (recommended)** — snapshot each account's current `CurrentBalance` as a single
|
||||||
|
dated "opening balance" JE at a cutover date; only new events post JEs after that. This is how real
|
||||||
|
systems migrate. With ~7 companies it is the pragmatic, low-risk path: pre-cutover per-transaction GL
|
||||||
|
granularity is lost, but every balance is preserved exactly.
|
||||||
|
|
||||||
|
## Phased plan (verifiable, not big-bang)
|
||||||
|
|
||||||
|
- **Phase 0** — build `PostAsync` + balanced-entry invariant + unit tests.
|
||||||
|
- **Phase 1** — route all posting sites through it, creating JEs *alongside* existing behavior (no report
|
||||||
|
changes yet). The **Balance Reconciliation report is the regression harness** — it should show near-zero
|
||||||
|
drift everywhere once every event posts a JE.
|
||||||
|
- **Phase 2** — switch `LedgerService` to JE-only; confirm `RecalculateAllAsync` reproduces balances.
|
||||||
|
- **Phase 3** — switch `FinancialReportService` (TB/BS/P&L) to JE-only; reconcile each report per company
|
||||||
|
against the old numbers using the snapshot harness below.
|
||||||
|
- **Phase 4** — post conversion-balance JEs at the cutover date; delete the dead re-derivation code
|
||||||
|
(~1,000+ lines).
|
||||||
|
- **Phase 5** — decide **O9** (perpetual vs periodic inventory) and implement it as JE postings. Perpetual
|
||||||
|
falls out naturally: purchases DR Inventory, consumption DR COGS / CR Inventory (O6 already posts the
|
||||||
|
consumption half). This makes the Balance Sheet inventory line consistent with the Trial Balance / P&L.
|
||||||
|
|
||||||
|
## Prerequisites before starting
|
||||||
|
|
||||||
|
- A **report-snapshot/diff harness** to compare each company's P&L / Balance Sheet / Trial Balance before
|
||||||
|
vs after each phase (the safety net for "numbers shifting").
|
||||||
|
- A **staging copy** of production data to run the cutover against first.
|
||||||
|
- A **decision on O9** (the inventory policy) — see `docs/ACCOUNTING_AUDIT.md`.
|
||||||
|
|
||||||
|
## Honest assessment
|
||||||
|
|
||||||
|
- **Effort:** the single biggest change in the accounting area — weeks, not hours, with careful per-company
|
||||||
|
verification.
|
||||||
|
- **Risk:** broad blast radius (every accounting controller); report numbers *will* shift slightly as they
|
||||||
|
become consistent (the point), so it needs per-company sign-off.
|
||||||
|
- **Optional:** after O1–O8 the current system is correct. The refactor buys *prevention* of this bug class,
|
||||||
|
a real audit trail, and ~1,500 fewer lines of parallel logic. Higher ROI the more accounting features you
|
||||||
|
keep adding.
|
||||||
|
- **Safe stopping point:** Phases 0–1 alone (post JEs everywhere, watch the reconciliation report) are
|
||||||
|
low-risk and immediately valuable. You can stop there and continue later.
|
||||||
|
|
||||||
|
## What it fixes
|
||||||
|
|
||||||
|
- Eliminates the O2/O6/O7/O8 bug class (one place to get each event right).
|
||||||
|
- Trial Balance balances by construction.
|
||||||
|
- Every balance is traceable to a journal entry (audit trail).
|
||||||
|
- Resolves **O9** as part of Phase 5.
|
||||||
|
- Turns the Balance Reconciliation report into a true integrity check rather than a drift detector.
|
||||||
Reference in New Issue
Block a user