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
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
Reference in New Issue
Block a user