Files
boc/aamos-ledger-refactor/test/double-entry.test.mjs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

173 lines
5.4 KiB
JavaScript

/**
* 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);
});
});