fix(accounting): opening balances were double-counted in three reports
The opening figures live in TWO places in this ledger: the
chart_of_accounts.opening_balance column AND 24 posted journal entries dated
2024-07-01 with reference_type='opening', totalling 90,601,962.36.
Three reports read the column and then also summed the ledger movement that
already contained those same entries, counting the opening twice:
- Trial balance (LedgerService::getTrialBalance)
- General ledger (LedgerService::getAccountLedger)
- Balance sheet (FinancialReportService::getBalanceSheet, and the
consolidated sheet which delegates to it)
Measured on live data, trial balance over FY 2024/2025:
1103 مشروعات تحت التنفيذ reported 85,627,410.75 actual 43,923,543.75
210201 أرباح مرحلة reported -146,645,270 actual -73,322,635
i.e. exactly double on every account carrying an opening balance. The report
still footed, because opening balances net to zero across debit and credit —
so it looked right and every line was wrong. Only periods containing
2024-07-01 were affected; a 2026 trial balance was already correct.
The opening column is now derived as cumulative posted movement BEFORE the
period start, which is the standard definition, removes the double count
structurally, and works for any period rather than only a year boundary. The
trial balance query is also restructured into two independent aggregates so
no row multiplication is possible and an account whose only movement predates
the period still appears.
Income statement was already correct and is unchanged.
Also in this commit:
- LedgerService::rebuildBalances() + a seed that runs it. The opening import
wrote journal rows without going through JournalService, so 24 accounts had
a cached current_balance disagreeing with the ledger — retained earnings
cached 0.00 against an actual 73,322,635.00. The reports read the ledger and
were fine, but the Chart of Accounts screen and the bank-reconciliation
opening figure read the cache, which is precisely where an accountant would
find a number contradicting the trial balance.
- Carnet guest entry never posted. Accounting listened on
'carnet.guest_entry_recorded'; GuestEntryService dispatches
'carnet_guest.entry_recorded' (underscore, not dot). Notifications listens on
the correct name, which is why notifications worked and the ledger entry
never appeared. Fees were recorded in carnet_guest_entries.amount_paid and
posted nowhere.
- 'tournament.fee_collected' has no dispatcher anywhere. Documented as dead
rather than left looking wired.
- Two fiscal years were flagged is_current; the seed leaves exactly the one
containing today. FiscalYear::findByDate now resolves overlapping years
deterministically (open first, then narrowest range) instead of taking
whatever the database returned — this chart has calendar years overlapping a
July-June year, so Jul-Dec 2024 matches two. No entry is reassigned; all 795
are already inside their assigned year.
- PostingRouter and postViaRule now probe App::db() with try/catch. It is typed
`: Database` and throws when unbound rather than returning null, so the
previous null guards could never fire.
Co-Authored-By:
Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
Please register or sign in to comment