diff --git a/backend/src/modules/accounts/accounts.service.ts b/backend/src/modules/accounts/accounts.service.ts index 36660dd..0a6d1b4 100644 --- a/backend/src/modules/accounts/accounts.service.ts +++ b/backend/src/modules/accounts/accounts.service.ts @@ -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( diff --git a/backend/src/modules/reports/reports.service.ts b/backend/src/modules/reports/reports.service.ts index c926abe..e0d921d 100644 --- a/backend/src/modules/reports/reports.service.ts +++ b/backend/src/modules/reports/reports.service.ts @@ -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, };