bae705aa97
- 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
173 lines
5.4 KiB
JavaScript
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);
|
|
});
|
|
});
|