• Mahmoud Aglan's avatar
    fix(accounting): opening balances were double-counted in three reports · 2f77d3e1
    Mahmoud Aglan authored
    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: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    2f77d3e1
FiscalYear.php 2.86 KB