/** * test/double-entry.test.mjs — Double-entry bookkeeping validation tests */ import { describe, it, before } from 'node:test'; import assert from 'node:assert'; // Mock pool for testing function createMockPool() { const queries = []; return { queries, query: async (sql, params) => { queries.push({ sql, params }); // Default responses if (sql.includes('SELECT status FROM ledger_periods')) { return { rows: [{ status: 'open' }] }; } if (sql.includes('INSERT INTO ledger_journal_entries')) { return { rows: [{ id: 'test-entry-id', tenant_id: params[0], fiscal_year: params[1], period: params[2], entry_date: params[3], description: params[4], status: 'draft' }] }; } if (sql.includes('INSERT INTO ledger_journal_lines')) { return { rows: [{ id: 'test-line-id', entry_id: params[0], line_number: params[2], account_number: params[3], debit: params[5], credit: params[6] }] }; } return { rows: [] }; }, connect: async () => ({ query: async (sql, params) => { queries.push({ sql, params, client: true }); if (sql === 'BEGIN') return {}; if (sql === 'COMMIT') return {}; if (sql === 'ROLLBACK') return {}; if (sql.includes('SELECT status FROM ledger_periods')) { return { rows: [{ status: 'open' }] }; } if (sql.includes('INSERT INTO ledger_journal_entries')) { return { rows: [{ id: 'test-entry-id', tenant_id: params[0], fiscal_year: params[1], period: params[2], entry_date: params[3], description: params[4], status: 'draft' }] }; } if (sql.includes('INSERT INTO ledger_journal_lines')) { return { rows: [{ id: 'test-line-id', entry_id: params[0], line_number: params[2], account_number: params[3], debit: params[5], credit: params[6] }] }; } return { rows: [] }; }, release: () => {} }) }; } // Simple double-entry validation (same logic as in routes) function validateDoubleEntry(lines) { const totalDebit = lines.reduce((s, l) => s + (parseFloat(l.debit) || 0), 0); const totalCredit = lines.reduce((s, l) => s + (parseFloat(l.credit) || 0), 0); return { valid: Math.abs(totalDebit - totalCredit) <= 0.01, totalDebit, totalCredit, diff: Math.abs(totalDebit - totalCredit) }; } describe('Double-entry validation', () => { it('should accept balanced entries (debit = credit)', () => { const lines = [ { account_number: '1930', debit: 1000, credit: 0 }, { account_number: '3000', debit: 0, credit: 1000 } ]; const result = validateDoubleEntry(lines); assert.strictEqual(result.valid, true); assert.strictEqual(result.totalDebit, 1000); assert.strictEqual(result.totalCredit, 1000); }); it('should reject unbalanced entries (debit ≠ credit)', () => { const lines = [ { account_number: '1930', debit: 1000, credit: 0 }, { account_number: '3000', debit: 0, credit: 500 } ]; const result = validateDoubleEntry(lines); assert.strictEqual(result.valid, false); assert.strictEqual(result.diff, 500); }); it('should handle multiple lines correctly', () => { const lines = [ { account_number: '1930', debit: 1000, credit: 0 }, { account_number: '2610', debit: 250, credit: 0 }, { account_number: '3000', debit: 0, credit: 1250 } ]; const result = validateDoubleEntry(lines); assert.strictEqual(result.valid, true); assert.strictEqual(result.totalDebit, 1250); assert.strictEqual(result.totalCredit, 1250); }); it('should handle zero amounts', () => { const lines = [ { account_number: '1930', debit: 0, credit: 0 }, { account_number: '3000', debit: 0, credit: 0 } ]; const result = validateDoubleEntry(lines); assert.strictEqual(result.valid, true); }); it('should handle floating point precision (0.01 tolerance)', () => { const lines = [ { account_number: '1930', debit: 100.005, credit: 0 }, { account_number: '3000', debit: 0, credit: 100 } ]; const result = validateDoubleEntry(lines); assert.strictEqual(result.valid, true); // 0.005 diff is within 0.01 tolerance }); }); describe('Line validation', () => { it('should require at least 2 lines', () => { const lines = [ { account_number: '1930', debit: 1000, credit: 0 } ]; assert.strictEqual(lines.length < 2, true); }); it('should require account_number on each line', () => { const lines = [ { account_number: '1930', debit: 1000, credit: 0 }, { account_number: null, debit: 0, credit: 1000 } ]; const hasMissingAccount = lines.some(l => !l.account_number); assert.strictEqual(hasMissingAccount, true); }); it('should require exactly one of debit or credit per line', () => { const linesBoth = [ { account_number: '1930', debit: 1000, credit: 500 } ]; const linesNone = [ { account_number: '1930', debit: null, credit: null } ]; const bothInvalid = (l) => (l.debit != null) === (l.credit != null); assert.strictEqual(bothInvalid(linesBoth[0]), true); assert.strictEqual(bothInvalid(linesNone[0]), true); }); });