fix: Phase C IPSAS matrix (2100/2410 HO legs) and runtime double-entry guards
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m16s
CI/CD Pipeline / Security Scanning (push) Successful in 2m39s
CI/CD Pipeline / Lint and Format (push) Failing after 50s
CI/CD Pipeline / Terraform Validation (push) Failing after 25s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 30s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 29s
Validation / validate-genesis (push) Successful in 30s
Validation / validate-terraform (push) Failing after 30s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m24s
Validation / validate-documentation (push) Failing after 20s
Verify Deployment / Verify Deployment (push) Failing after 1m7s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m16s
CI/CD Pipeline / Security Scanning (push) Successful in 2m39s
CI/CD Pipeline / Lint and Format (push) Failing after 50s
CI/CD Pipeline / Terraform Validation (push) Failing after 25s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 30s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 29s
Validation / validate-genesis (push) Successful in 30s
Validation / validate-terraform (push) Failing after 30s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m24s
Validation / validate-documentation (push) Failing after 20s
Verify Deployment / Verify Deployment (push) Failing after 1m7s
Align interoffice HO allocations to 2100→2410 with branch 1410→2100 two-leg entries, registry v1.2.1, explicit same-GL rejection in validators, doc matrices, and validation tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { omnlSensitiveRouteGuard, omnlRequireApiKeyInProduction } from '../middl
|
||||
import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
|
||||
import {
|
||||
loadIpsasRegistry,
|
||||
isDoubleEntryPair,
|
||||
validateJournalPairWithMatrix,
|
||||
fetchFineractGlAccounts,
|
||||
compareRegistryToFineract,
|
||||
@@ -78,7 +79,9 @@ router.post('/omnl/ipsas/validate-pairs', (req: Request, res: Response) => {
|
||||
return {
|
||||
debitGlCode,
|
||||
creditGlCode,
|
||||
doubleEntryValid: debitGlCode !== creditGlCode,
|
||||
ipsasCompliantPair: v.valid,
|
||||
rejectReason: v.rejectReason,
|
||||
ipsasRef: v.ipsasRef,
|
||||
memo: v.memo,
|
||||
narrative: v.narrative,
|
||||
@@ -107,13 +110,16 @@ router.get('/omnl/ipsas/validate-pair', (req: Request, res: Response) => {
|
||||
} catch {
|
||||
matrix = null;
|
||||
}
|
||||
const doubleEntryValid = isDoubleEntryPair(debitGlCode, creditGlCode);
|
||||
const v = validateJournalPairWithMatrix(registry, matrix, debitGlCode, creditGlCode);
|
||||
const debitKnown = registry.accounts.some((a) => a.glCode === debitGlCode);
|
||||
const creditKnown = registry.accounts.some((a) => a.glCode === creditGlCode);
|
||||
res.json({
|
||||
debitGlCode,
|
||||
creditGlCode,
|
||||
doubleEntryValid,
|
||||
ipsasCompliantPair: v.valid,
|
||||
rejectReason: v.rejectReason,
|
||||
ipsasRef: v.ipsasRef,
|
||||
memo: v.memo,
|
||||
narrative: v.narrative,
|
||||
|
||||
@@ -45,15 +45,19 @@ export function loadIpsasRegistry(): IpsasGlRegistry {
|
||||
return JSON.parse(readFileSync(p, 'utf8')) as IpsasGlRegistry;
|
||||
}
|
||||
|
||||
export function isDoubleEntryPair(debitGlCode: string, creditGlCode: string): boolean {
|
||||
return debitGlCode.trim() !== creditGlCode.trim();
|
||||
}
|
||||
|
||||
export function validateJournalPair(
|
||||
registry: IpsasGlRegistry,
|
||||
debitGlCode: string,
|
||||
creditGlCode: string
|
||||
): { valid: boolean; ipsasRef?: string; memo?: string } {
|
||||
): { valid: boolean; ipsasRef?: string; memo?: string; rejectReason?: string } {
|
||||
const d = debitGlCode.trim();
|
||||
const c = creditGlCode.trim();
|
||||
if (d === c) {
|
||||
return { valid: false };
|
||||
if (!isDoubleEntryPair(d, c)) {
|
||||
return { valid: false, rejectReason: 'same_gl_account' };
|
||||
}
|
||||
const hit = registry.allowedJournalPairs.find(
|
||||
(p) => p.debitGlCode === d && p.creditGlCode === c
|
||||
@@ -70,16 +74,16 @@ export function validateJournalPairWithMatrix(
|
||||
matrix: JournalMatrixFile | null,
|
||||
debitGlCode: string,
|
||||
creditGlCode: string
|
||||
): { valid: boolean; ipsasRef?: string; memo?: string; narrative?: string; source: 'registry' | 'matrix' | 'none' } {
|
||||
): { valid: boolean; ipsasRef?: string; memo?: string; narrative?: string; source: 'registry' | 'matrix' | 'none'; rejectReason?: string } {
|
||||
const d = debitGlCode.trim();
|
||||
const c = creditGlCode.trim();
|
||||
if (!isDoubleEntryPair(d, c)) {
|
||||
return { valid: false, source: 'none', rejectReason: 'same_gl_account' };
|
||||
}
|
||||
const r0 = validateJournalPair(registry, debitGlCode, creditGlCode);
|
||||
if (r0.valid) {
|
||||
return { ...r0, source: 'registry' };
|
||||
}
|
||||
const d = debitGlCode.trim();
|
||||
const c = creditGlCode.trim();
|
||||
if (d === c) {
|
||||
return { valid: false, source: 'none' };
|
||||
}
|
||||
if (matrix) {
|
||||
const hit = matrix.entries.find((e) => e.debitGlCode === d && e.creditGlCode === c);
|
||||
if (hit) {
|
||||
@@ -92,7 +96,7 @@ export function validateJournalPairWithMatrix(
|
||||
};
|
||||
}
|
||||
}
|
||||
return { valid: false, source: 'none' };
|
||||
return { valid: false, source: 'none', rejectReason: 'not_in_allowlist' };
|
||||
}
|
||||
|
||||
export function assertGlCodeKnown(registry: IpsasGlRegistry, glCode: string): boolean {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { loadIpsasRegistry, validateJournalPair, validateJournalPairWithMatrix } from './omnl-ipsas-gl';
|
||||
import { loadJournalMatrix } from './omnl-journal-matrix';
|
||||
|
||||
describe('omnl IPSAS journal validation', () => {
|
||||
const registry = loadIpsasRegistry();
|
||||
const matrix = loadJournalMatrix();
|
||||
|
||||
it('rejects same-GL pairs (invalid double entry)', () => {
|
||||
const v = validateJournalPair(registry, '2000', '2000');
|
||||
expect(v.valid).toBe(false);
|
||||
expect(v.rejectReason).toBe('same_gl_account');
|
||||
const vm = validateJournalPairWithMatrix(registry, matrix, '2000', '2000');
|
||||
expect(vm.valid).toBe(false);
|
||||
expect(vm.rejectReason).toBe('same_gl_account');
|
||||
});
|
||||
|
||||
it('accepts Phase C Office 24 branch leg', () => {
|
||||
const v = validateJournalPairWithMatrix(registry, matrix, '1410', '2100');
|
||||
expect(v.valid).toBe(true);
|
||||
expect(v.memo).toMatch(/H24|INTEROFFICE/);
|
||||
});
|
||||
|
||||
it('accepts Phase C HO interoffice leg 2100→2410', () => {
|
||||
const v = validateJournalPairWithMatrix(registry, matrix, '2100', '2410');
|
||||
expect(v.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts M2→M3 token load pair', () => {
|
||||
const v = validateJournalPairWithMatrix(registry, matrix, '2200', '2300');
|
||||
expect(v.valid).toBe(true);
|
||||
expect(v.memo).toBe('T-M2M3');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user