diff --git a/lib/reports/__tests__/monthly-breakdown.test.ts b/lib/reports/__tests__/monthly-breakdown.test.ts index 5101ff44..70015bcc 100644 --- a/lib/reports/__tests__/monthly-breakdown.test.ts +++ b/lib/reports/__tests__/monthly-breakdown.test.ts @@ -105,26 +105,26 @@ describe('generateMonthlyBreakdown', () => { data: [ { account_number: '3001', - debit: 0, - credit: 10000, + debit_amount: 0, + credit_amount: 10000, journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, }, { account_number: '5010', - debit: 3000, - credit: 0, + debit_amount: 3000, + credit_amount: 0, journal_entry: { entry_date: '2024-01-20', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, }, { account_number: '3001', - debit: 0, - credit: 5000, + debit_amount: 0, + credit_amount: 5000, journal_entry: { entry_date: '2024-02-10', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, }, { account_number: '6200', - debit: 1500, - credit: 0, + debit_amount: 1500, + credit_amount: 0, journal_entry: { entry_date: '2024-02-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, }, ], @@ -156,7 +156,7 @@ describe('generateMonthlyBreakdown', () => { expect(mar.expenses).toBe(0) }) - it('ignores non-revenue/expense accounts (class 1, 2, 8)', async () => { + it('ignores balance sheet accounts (class 1, 2) but includes class 8 financial items', async () => { let callCount = 0 supabase.from.mockImplementation(() => { callCount++ @@ -184,22 +184,28 @@ describe('generateMonthlyBreakdown', () => { data: [ { account_number: '1930', - debit: 10000, - credit: 0, + debit_amount: 10000, + credit_amount: 0, journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, }, { account_number: '2611', - debit: 0, - credit: 2500, + debit_amount: 0, + credit_amount: 2500, journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, }, { - account_number: '8999', - debit: 500, - credit: 0, + account_number: '8400', + debit_amount: 500, + credit_amount: 0, journal_entry: { entry_date: '2024-01-20', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, }, + { + account_number: '8300', + debit_amount: 0, + credit_amount: 200, + journal_entry: { entry_date: '2024-01-25', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' }, + }, ], error: null, }), @@ -211,7 +217,10 @@ describe('generateMonthlyBreakdown', () => { const result = await generateMonthlyBreakdown('user-1', 'period-1') const jan = result.months.find((m) => m.label === 'Jan')! - expect(jan.income).toBe(0) - expect(jan.expenses).toBe(0) + // Class 1 and 2 are ignored + // Class 8 debit (8400 interest expense) → expense + expect(jan.expenses).toBe(500) + // Class 8 credit (8300 interest income) → income + expect(jan.income).toBe(200) }) }) diff --git a/lib/reports/ar-reconciliation.ts b/lib/reports/ar-reconciliation.ts index b01ca329..358408ef 100644 --- a/lib/reports/ar-reconciliation.ts +++ b/lib/reports/ar-reconciliation.ts @@ -13,7 +13,7 @@ export interface ARReconciliationResult { */ export async function generateARReconciliation( userId: string, - _periodId: string + periodId: string ): Promise { const supabase = await createClient() @@ -25,20 +25,31 @@ export async function generateARReconciliation( .in('status', ['sent', 'overdue']) const arLedgerTotal = (invoices || []) - .reduce((sum, inv) => sum + ((Number(inv.total) || 0) - (Number(inv.paid_amount) || 0)), 0) + .reduce((sum, inv) => Math.round((sum + (Number(inv.total) || 0) - (Number(inv.paid_amount) || 0)) * 100) / 100, 0) - // Get account 1510 balance from journal entry lines + // Get account 1510 balance from posted journal entry lines in this period const { data: journalLines } = await supabase .from('journal_entry_lines') - .select('debit_amount, credit_amount, journal_entry_id') + .select(` + debit_amount, + credit_amount, + journal_entry:journal_entries!inner( + status, + user_id, + fiscal_period_id + ) + `) .eq('account_number', '1510') + .eq('journal_entries.user_id', userId) + .eq('journal_entries.fiscal_period_id', periodId) + .eq('journal_entries.status', 'posted') // Account 1510 is an asset: debit normal balance // Balance = debits - credits let account1510Balance = 0 if (journalLines) { for (const line of journalLines) { - account1510Balance += (Number(line.debit_amount) || 0) - (Number(line.credit_amount) || 0) + account1510Balance = Math.round((account1510Balance + (Number(line.debit_amount) || 0) - (Number(line.credit_amount) || 0)) * 100) / 100 } } diff --git a/lib/reports/general-ledger.ts b/lib/reports/general-ledger.ts index fdd0f062..0fc9e1ab 100644 --- a/lib/reports/general-ledger.ts +++ b/lib/reports/general-ledger.ts @@ -57,7 +57,7 @@ export async function generateGeneralLedger( .select('id, entry_date, voucher_number, voucher_series, description, source_type') .eq('user_id', userId) .eq('fiscal_period_id', periodId) - .in('status', ['posted', 'reversed']) + .eq('status', 'posted') if (!entries || entries.length === 0) { return { accounts: [], period: { start: period.period_start, end: period.period_end } } @@ -95,7 +95,7 @@ export async function generateGeneralLedger( .from('journal_entries') .select('id') .eq('user_id', userId) - .in('status', ['posted', 'reversed']) + .eq('status', 'posted') .lt('entry_date', period.period_start) const openingBalances = new Map() diff --git a/lib/reports/monthly-breakdown.ts b/lib/reports/monthly-breakdown.ts index a76c1818..76d96d21 100644 --- a/lib/reports/monthly-breakdown.ts +++ b/lib/reports/monthly-breakdown.ts @@ -46,8 +46,8 @@ export async function generateMonthlyBreakdown( .from('journal_entry_lines') .select(` account_number, - debit, - credit, + debit_amount, + credit_amount, journal_entry:journal_entries!inner( entry_date, status, @@ -63,17 +63,21 @@ export async function generateMonthlyBreakdown( return { months: [] } } - // Build monthly aggregates - const monthMap = new Map() + // Build monthly aggregates using year-aware keys ("2024-03", "2024-04", etc.) + // to avoid data corruption for non-calendar fiscal years (e.g., Apr-Mar) + const monthMap = new Map() // Initialize all months in the period range const startDate = new Date(period.period_start) const endDate = new Date(period.period_end) - const startMonth = startDate.getMonth() - const endMonth = endDate.getMonth() + (endDate.getFullYear() - startDate.getFullYear()) * 12 - for (let m = startMonth; m <= endMonth; m++) { - monthMap.set(m % 12, { income: 0, expenses: 0 }) + for ( + let y = startDate.getFullYear(), m = startDate.getMonth(); + y < endDate.getFullYear() || (y === endDate.getFullYear() && m <= endDate.getMonth()); + m === 11 ? (y++, m = 0) : m++ + ) { + const key = `${y}-${String(m).padStart(2, '0')}` + monthMap.set(key, { year: y, month: m, income: 0, expenses: 0 }) } for (const line of lines) { @@ -85,35 +89,39 @@ export async function generateMonthlyBreakdown( } const accountClass = parseInt(line.account_number.charAt(0)) const entryDate = new Date(entry.entry_date) - const month = entryDate.getMonth() + const key = `${entryDate.getFullYear()}-${String(entryDate.getMonth()).padStart(2, '0')}` - if (!monthMap.has(month)) { - monthMap.set(month, { income: 0, expenses: 0 }) + if (!monthMap.has(key)) { + monthMap.set(key, { year: entryDate.getFullYear(), month: entryDate.getMonth(), income: 0, expenses: 0 }) } - const bucket = monthMap.get(month)! + const bucket = monthMap.get(key)! if (accountClass === 3) { // Revenue accounts: credit side represents revenue - bucket.income = Math.round((bucket.income + line.credit - line.debit) * 100) / 100 + bucket.income = Math.round((bucket.income + line.credit_amount - line.debit_amount) * 100) / 100 } else if (accountClass >= 4 && accountClass <= 7) { // Expense accounts: debit side represents expenses - bucket.expenses = Math.round((bucket.expenses + line.debit - line.credit) * 100) / 100 + bucket.expenses = Math.round((bucket.expenses + line.debit_amount - line.credit_amount) * 100) / 100 + } else if (accountClass === 8) { + // Financial items (class 8): interest, exchange gains/losses, etc. + const amount = line.credit_amount - line.debit_amount + if (amount >= 0) { + bucket.income = Math.round((bucket.income + amount) * 100) / 100 + } else { + bucket.expenses = Math.round((bucket.expenses + Math.abs(amount)) * 100) / 100 + } } } - // Convert to sorted array + // Convert to sorted array (keys sort naturally as "YYYY-MM") const months: MonthlyBreakdownMonth[] = [] - const sortedMonths = Array.from(monthMap.entries()).sort((a, b) => { - // Handle year boundaries (e.g., Nov-Dec-Jan for broken fiscal year) - const aAdj = a[0] < startMonth ? a[0] + 12 : a[0] - const bAdj = b[0] < startMonth ? b[0] + 12 : b[0] - return aAdj - bAdj - }) + const sortedKeys = Array.from(monthMap.keys()).sort() - for (const [month, data] of sortedMonths) { + for (const key of sortedKeys) { + const data = monthMap.get(key)! months.push({ - label: MONTH_LABELS[month], + label: MONTH_LABELS[data.month], income: data.income, expenses: data.expenses, net: Math.round((data.income - data.expenses) * 100) / 100, diff --git a/lib/reports/sie-export.ts b/lib/reports/sie-export.ts index 4b34263f..2c694423 100644 --- a/lib/reports/sie-export.ts +++ b/lib/reports/sie-export.ts @@ -195,7 +195,8 @@ function dateStringToSIE(dateStr: string): string { * Format amount for SIE (no thousands separator, . as decimal) */ function formatAmount(amount: number): string { - return amount.toFixed(2) + const rounded = Math.round(amount * 100) / 100 + return rounded.toFixed(2) } /** @@ -218,7 +219,7 @@ function calculateBalances( for (const line of lines) { const current = balances.get(line.account_number) || 0 const netAmount = (Number(line.debit_amount) || 0) - (Number(line.credit_amount) || 0) - balances.set(line.account_number, current + netAmount) + balances.set(line.account_number, Math.round((current + netAmount) * 100) / 100) } } diff --git a/lib/reports/supplier-reconciliation.ts b/lib/reports/supplier-reconciliation.ts index 51f56743..9f7a2aba 100644 --- a/lib/reports/supplier-reconciliation.ts +++ b/lib/reports/supplier-reconciliation.ts @@ -24,21 +24,31 @@ export async function generateReconciliation( .in('status', ['registered', 'approved', 'partially_paid', 'overdue']) const supplierLedgerTotal = (invoices || []) - .reduce((sum, inv) => sum + (inv.remaining_amount || 0), 0) + .reduce((sum, inv) => Math.round((sum + (inv.remaining_amount || 0)) * 100) / 100, 0) - // Get account 2440 balance from journal entry lines + // Get account 2440 balance from posted journal entry lines in this period const { data: journalLines } = await supabase .from('journal_entry_lines') - .select('debit_amount, credit_amount, journal_entry_id') + .select(` + debit_amount, + credit_amount, + journal_entry:journal_entries!inner( + status, + user_id, + fiscal_period_id + ) + `) .eq('account_number', '2440') + .eq('journal_entries.user_id', userId) + .eq('journal_entries.fiscal_period_id', periodId) + .eq('journal_entries.status', 'posted') - // Filter to posted entries in the period + // Account 2440 is a liability: credit normal balance + // Balance = credits - debits let account2440Balance = 0 if (journalLines) { - // Account 2440 is a liability: credit normal balance - // Balance = credits - debits for (const line of journalLines) { - account2440Balance += (line.credit_amount || 0) - (line.debit_amount || 0) + account2440Balance = Math.round((account2440Balance + (line.credit_amount || 0) - (line.debit_amount || 0)) * 100) / 100 } }