Fix account balances showing $0 and dashboard KPIs on new tenants

Root cause: multiple issues with balance computation across the system.

Accounts list:
- findAll() was doing SELECT * FROM accounts, returning the stale
  denormalized `balance` column (always 0). Now computes balances from
  journal entries using proper double-entry logic (debit-credit for
  assets/expenses, credit-debit for liabilities/equity/income).

Dashboard KPIs:
- Total Cash filtered by name LIKE '%Cash%' which missed accounts not
  named "Cash". Now queries ALL asset accounts regardless of name.
- Reserve Fund queried the legacy reserve_components.current_fund_balance
  column. Now computes from journal entries on reserve asset accounts.

Opening balance journal entries:
- On blank tenants, equity offset accounts (3000/3100) don't exist, so
  the balancing journal entry line was silently skipped, leaving entries
  unbalanced. Now auto-creates Operating Fund Balance (3000) and Reserve
  Fund Balance (3100) equity accounts when needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 19:11:00 -05:00
parent b0634f7263
commit 739ccaeed4
2 changed files with 56 additions and 16 deletions

View File

@@ -394,28 +394,44 @@ export class ReportsService {
}
async getDashboardKPIs() {
// Total cash (all asset accounts with 'Cash' in name)
// Total cash: ALL asset accounts (not just those named "Cash")
// Uses proper double-entry balance: debit - credit for assets
const cash = await this.tenant.query(`
SELECT COALESCE(SUM(sub.balance), 0) as total FROM (
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as balance
FROM accounts a
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
WHERE a.account_type = 'asset' AND a.name LIKE '%Cash%' AND a.is_active = true
WHERE a.account_type = 'asset' AND a.is_active = true
GROUP BY a.id
) sub
`);
const totalCash = parseFloat(cash[0]?.total || '0');
// Receivables
// Receivables: sum of unpaid invoices
const ar = await this.tenant.query(`
SELECT COALESCE(SUM(amount - amount_paid), 0) as total
FROM invoices WHERE status NOT IN ('paid', 'void', 'written_off')
`);
// Reserve fund balance
// Reserve fund balance: computed from journal entries on reserve fund_type accounts
// credit - debit for equity/liability/income accounts (reserve equity + reserve income - reserve expenses)
const reserves = await this.tenant.query(`
SELECT COALESCE(SUM(current_fund_balance), 0) as total FROM reserve_components
SELECT COALESCE(SUM(sub.balance), 0) as total FROM (
SELECT
CASE
WHEN a.account_type IN ('asset')
THEN COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0)
WHEN a.account_type IN ('expense')
THEN -(COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0))
ELSE COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0)
END as balance
FROM accounts a
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
WHERE a.fund_type = 'reserve' AND a.account_type IN ('asset') AND a.is_active = true
GROUP BY a.id, a.account_type
) sub
`);
// Delinquent count (overdue invoices)
@@ -434,7 +450,7 @@ export class ReportsService {
return {
total_cash: totalCash.toFixed(2),
total_receivables: ar[0]?.total || '0.00',
reserve_fund_balance: reserves[0]?.total || '0.00',
reserve_fund_balance: parseFloat(reserves[0]?.total || '0').toFixed(2),
delinquent_units: parseInt(delinquent[0]?.count || '0'),
recent_transactions: recentTx,
};