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
96 lines
3.6 KiB
TypeScript
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();
|
|
});
|