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

@@ -8,22 +8,34 @@ export class AccountsService {
constructor(private tenant: TenantService) {}
async findAll(fundType?: string, includeArchived?: boolean) {
let sql = 'SELECT * FROM accounts';
const params: any[] = [];
const conditions: string[] = [];
const params: any[] = [];
if (!includeArchived) {
conditions.push('is_active = true');
conditions.push('a.is_active = true');
}
if (fundType) {
params.push(fundType);
conditions.push(`fund_type = $${params.length}`);
conditions.push(`a.fund_type = $${params.length}`);
}
if (conditions.length) {
sql += ' WHERE ' + conditions.join(' AND ');
}
sql += ' ORDER BY account_number';
const whereClause = conditions.length ? 'WHERE ' + conditions.join(' AND ') : '';
const sql = `
SELECT a.*,
CASE
WHEN a.account_type IN ('asset', '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
${whereClause}
GROUP BY a.id
ORDER BY a.account_number
`;
return this.tenant.query(sql, params);
}
@@ -79,12 +91,24 @@ export class AccountsService {
const acctDebit = isDebitNormal ? absAmount : 0;
const acctCredit = isDebitNormal ? 0 : absAmount;
// Determine equity offset account based on fund type
// Determine equity offset account based on fund type (auto-create if missing)
const equityAccountNumber = dto.fundType === 'reserve' ? 3100 : 3000;
const equityRows = await this.tenant.query(
const equityName = dto.fundType === 'reserve' ? 'Reserve Fund Balance' : 'Operating Fund Balance';
let equityRows = await this.tenant.query(
'SELECT id FROM accounts WHERE account_number = $1',
[equityAccountNumber],
);
if (!equityRows.length) {
await this.tenant.query(
`INSERT INTO accounts (account_number, name, account_type, fund_type, is_system)
VALUES ($1, $2, 'equity', $3, true)`,
[equityAccountNumber, equityName, dto.fundType],
);
equityRows = await this.tenant.query(
'SELECT id FROM accounts WHERE account_number = $1',
[equityAccountNumber],
);
}
// Create the journal entry
const jeInsert = await this.tenant.query(