Files
cross-financial-management/scripts/smokeTest.ts
T
Joshua OosthuizenandClaude Sonnet 5 4acbc03b35 Initial commit: multi-company financial management system
Root company app (src/, client/) — GL foundation, AR, and AP:
- Segmented Chart of Accounts (Sage 300-style), double-entry PostingService with
  three-layer balance enforcement (app validation, CHECK constraints, deferred
  DB trigger), gap-free document numbering, RBAC with branch restrictions.
- Accounts Receivable: Customers, Invoices, Credit Notes, Receipts with
  allocation, all posting through PostingService.
- Accounts Payable: Suppliers, Supplier Invoices, Debit Notes, Payments with
  allocation, mirroring AR's posting pattern.
- Tax Codes shared between AR and AP.
- Company-app login is a token handoff from the Account Manager (RS256,
  single-use, company-scoped) rather than a local password.
- Frontend reskinned to the approved mockup: navy sidebar, dashboard, tree-view
  Chart of Accounts, Customer/Supplier 360 pages.

Account Manager (manager/, manager/client/) — central login gateway:
- Password + TOTP 2FA + Google/Microsoft OAuth, company picker, and the RS256
  handoff token issuance that the company app verifies.

Covered by unit + integration tests against a real Postgres database (GL
balance invariants, AR/AP posting correctness and allocation limits, the full
handoff exchange including replay/cross-company/expiry/forgery rejection).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tq2DbEvzHzs13jDFhLaX44
2026-09-03 13:27:19 +02:00

96 lines
3.6 KiB
TypeScript

/**
* Phase 1 smoke test: posts a balanced manual journal entry, confirms an unbalanced one is
* rejected, then prints a simple Main + per-branch debit/credit sum to confirm everything
* still balances. Run after `npm run prisma:seed` against a real database:
* npm run smoke
*/
import { PrismaClient } from '@prisma/client';
import { PostingService } from '../src/modules/gl/posting.service';
import { UnbalancedEntryError } from '../src/modules/gl/posting.types';
const prisma = new PrismaClient();
async function getAccount(accountCode: string) {
return prisma.gLAccount.findUniqueOrThrow({ where: { accountCode } });
}
async function main() {
const admin = await prisma.user.findUniqueOrThrow({ where: { email: 'joosth2002@icloud.com' } });
const bankHQ = await getAccount('1000-000-000-HQ');
const salesHQ = await getAccount('4000-000-000-HQ');
const bankAMA = await getAccount('1000-000-000-AMA');
const salesAMA = await getAccount('4000-000-000-AMA');
console.log('Posting a balanced entry at HQ...');
const hqEntry = await PostingService.postEntry({
journalCode: 'GJ',
entryDate: new Date(),
description: 'Smoke test cash sale — HQ',
sourceModule: 'GL',
sourceType: 'SmokeTest',
createdById: admin.id,
lines: [
{ glAccountId: bankHQ.id, debit: 1000, credit: 0, branchSegmentValueId: bankHQ.id },
{ glAccountId: salesHQ.id, debit: 0, credit: 1000, branchSegmentValueId: salesHQ.id },
],
});
console.log(` Posted ${hqEntry.entryNumber}: debits ${hqEntry.totalDebits} / credits ${hqEntry.totalCredits}`);
console.log('Posting a balanced entry at AMA...');
const amaEntry = await PostingService.postEntry({
journalCode: 'GJ',
entryDate: new Date(),
description: 'Smoke test cash sale — AMA',
sourceModule: 'GL',
sourceType: 'SmokeTest',
createdById: admin.id,
lines: [
{ glAccountId: bankAMA.id, debit: 400, credit: 0 },
{ glAccountId: salesAMA.id, debit: 0, credit: 400 },
],
});
console.log(` Posted ${amaEntry.entryNumber}: debits ${amaEntry.totalDebits} / credits ${amaEntry.totalCredits}`);
console.log('Attempting an unbalanced entry (should be rejected)...');
try {
await PostingService.postEntry({
journalCode: 'GJ',
entryDate: new Date(),
description: 'This should never persist',
sourceModule: 'GL',
sourceType: 'SmokeTest',
createdById: admin.id,
lines: [
{ glAccountId: bankHQ.id, debit: 100, credit: 0 },
{ glAccountId: salesHQ.id, debit: 0, credit: 50 },
],
});
throw new Error('Unbalanced entry was accepted — this is a bug in PostingService');
} catch (error) {
if (error instanceof UnbalancedEntryError) {
console.log(` Correctly rejected: ${error.message}`);
} else {
throw error;
}
}
console.log('Trial balance check (sum of all posted lines, grouped by account type)...');
const lines = await prisma.journalEntryLine.findMany({ include: { glAccount: true } });
const totalDebits = lines.reduce((total, line) => total + Number(line.debit), 0);
const totalCredits = lines.reduce((total, line) => total + Number(line.credit), 0);
console.log(` Main: total debits ${totalDebits.toFixed(2)}, total credits ${totalCredits.toFixed(2)}`);
if (totalDebits !== totalCredits) {
throw new Error('Trial balance does not sum to zero — double-entry invariant violated');
}
console.log('Smoke test passed: every posted entry balances.');
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});