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:
@@ -11,7 +11,7 @@
|
||||
"headOfficeDebitGlCode": "2100",
|
||||
"headOfficeCreditGlCode": "2410",
|
||||
"materialOpeningUsd": 1000000000,
|
||||
"journalMatrixDoc": "config/omnl-journal-matrix.json",
|
||||
"journalMatrixDoc": "docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix-hospitallers24-opening-1b.json",
|
||||
"runbookDoc": "docs/04-configuration/mifos-omnl-central-bank/HOSPITALLERS24_OFFICE_OPENING_1B_M1.md",
|
||||
"currency": "USD",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "IPSAS-aligned GL codes for OMNL Hybx (Fineract). Includes core M0/M1, FX/XAU (IAS 21), fees, and IAS 37 provisions.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"currencyCode": "USD",
|
||||
"accounts": [
|
||||
{
|
||||
@@ -117,18 +117,18 @@
|
||||
},
|
||||
{
|
||||
"glCode": "2000",
|
||||
"name": "USD Central Deposits (M1)",
|
||||
"name": "USD Central Deposits (M1 pool)",
|
||||
"fineractType": "LIABILITY",
|
||||
"usage": "Demand deposits; financial liability",
|
||||
"usage": "Head Office central M1 pool / migration control; Phase C spendable M1 at branches uses 2100",
|
||||
"ipsasStandards": ["IPSAS 28", "IPSAS 29"],
|
||||
"ifrsRefs": ["IAS 32", "IFRS 7"],
|
||||
"roles": ["m1_liability", "demand_deposit"]
|
||||
},
|
||||
{
|
||||
"glCode": "2100",
|
||||
"name": "USD Restricted / Escrow (M1)",
|
||||
"name": "USD M1 Circulating (branch spendable)",
|
||||
"fineractType": "LIABILITY",
|
||||
"usage": "Restricted liabilities; escrow / fiduciary",
|
||||
"usage": "Branch spendable M1 / circulating eMoney (Phase C, Office 24 glM1); interoffice HO leg debits this account",
|
||||
"ipsasStandards": ["IPSAS 28", "IPSAS 29"],
|
||||
"ifrsRefs": ["IAS 32"],
|
||||
"roles": ["m1_restricted", "escrow", "fiduciary"]
|
||||
@@ -220,7 +220,8 @@
|
||||
"allowedJournalPairs": [
|
||||
{ "debitGlCode": "1000", "creditGlCode": "2000", "ipsasRef": "IPSAS 3, 28", "memo": "T-001", "ifrsRef": "IAS 32 issuance" },
|
||||
{ "debitGlCode": "1050", "creditGlCode": "2000", "ipsasRef": "IPSAS 28, 29", "memo": "T-001B", "ifrsRef": "IAS 32 M0 reserve" },
|
||||
{ "debitGlCode": "2000", "creditGlCode": "2410", "ipsasRef": "IPSAS 28", "memo": "T-002A", "ifrsRef": "IAS 32 inter-office allocation" },
|
||||
{ "debitGlCode": "2100", "creditGlCode": "2410", "ipsasRef": "IPSAS 28", "memo": "INTEROFFICE-HO", "ifrsRef": "IAS 32 Phase C HO leg (2100→2410)" },
|
||||
{ "debitGlCode": "1410", "creditGlCode": "2100", "ipsasRef": "IPSAS 28", "memo": "INTEROFFICE-BR", "ifrsRef": "IAS 32 Phase C branch leg (1410→2100)" },
|
||||
{ "debitGlCode": "2000", "creditGlCode": "2100", "ipsasRef": "IPSAS 28", "memo": "T-002B" },
|
||||
{ "debitGlCode": "2100", "creditGlCode": "2410", "ipsasRef": "IPSAS 28", "memo": "T-H24-HO", "ifrsRef": "IAS 32 HO opening leg" },
|
||||
{ "debitGlCode": "1410", "creditGlCode": "2100", "ipsasRef": "IPSAS 28", "memo": "T-H24-BR", "ifrsRef": "IAS 32 branch opening leg" },
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"description": "IPSAS double-entry journal matrix for OMNL Hybx (Fineract). Every entry debits one GL and credits a distinct GL with equal amount — no same-account pairs. Head Office (officeId=1) and branch legs identified by memo / officeId.",
|
||||
"description": "IPSAS double-entry journal matrix for OMNL Hybx (Fineract). Every entry debits one GL and credits a distinct GL. Phase C interoffice: HO leg 2100→2410, branch leg 1410→2100. Served path: smom-dbis-138/config/omnl-journal-matrix.json (override OMNL_JOURNAL_MATRIX_PATH).",
|
||||
"source": "OMNL_IPSAS_COMPLIANCE_MATRIX",
|
||||
"currencyCode": "USD",
|
||||
"dateFormat": "yyyy-MM-dd",
|
||||
"locale": "en",
|
||||
"doubleEntryRule": "debitGlCode !== creditGlCode; sum(debits) === sum(credits) per journal",
|
||||
"phaseCInterofficeRule": "HO officeId=1: Dr 2100 M1 circulating / Cr 2410 due to branch; branch office: Dr 1410 due from HO / Cr 2100 M1 circulating",
|
||||
"entries": [
|
||||
{
|
||||
"memo": "T-001",
|
||||
@@ -12,7 +13,7 @@
|
||||
"debitGlCode": "1000",
|
||||
"creditGlCode": "2000",
|
||||
"amount": 900000000000,
|
||||
"narrative": "Opening Balance Migration (Head Office) — Dr settlement asset / Cr M1 central deposits",
|
||||
"narrative": "Opening Balance Migration (Head Office) — Dr settlement asset / Cr M1 central pool",
|
||||
"ipsasRef": "IPSAS 3, 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
@@ -22,17 +23,27 @@
|
||||
"debitGlCode": "1050",
|
||||
"creditGlCode": "2000",
|
||||
"amount": 250000000000,
|
||||
"narrative": "Treasury Conversion — Dr M0 reserve / Cr M1 central deposits (Head Office)",
|
||||
"narrative": "Treasury Conversion — Dr M0 reserve / Cr M1 central pool (Head Office)",
|
||||
"ipsasRef": "IPSAS 28, 29",
|
||||
"moneyLayer": "M0"
|
||||
},
|
||||
{
|
||||
"memo": "T-002A",
|
||||
"memo": "T-002A-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2000",
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 2900000000,
|
||||
"narrative": "HO M1 allocation to Office 2 — Dr central deposits / Cr due to branch",
|
||||
"narrative": "Phase C HO leg — Shamrayan Available (M1) Office 2: Dr M1 circulating / Cr due to branch",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-002A-BR",
|
||||
"officeId": 2,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 2900000000,
|
||||
"narrative": "Phase C branch leg — Shamrayan Available (M1) Office 2: Dr due from HO / Cr M1 circulating",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
@@ -42,7 +53,7 @@
|
||||
"debitGlCode": "2000",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 2100000000,
|
||||
"narrative": "Shamrayan Restricted — Dr M1 central / Cr M1 restricted (Office 2)",
|
||||
"narrative": "Shamrayan Restricted — Dr M1 central pool / Cr M1 restricted escrow (Office 2)",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
@@ -52,57 +63,107 @@
|
||||
"debitGlCode": "2000",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 350000000000,
|
||||
"narrative": "HYBX Capitalization Escrow — Dr M1 central / Cr M1 restricted (Office 3)",
|
||||
"narrative": "HYBX Capitalization Escrow — Dr M1 central pool / Cr M1 restricted (Office 3)",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-004",
|
||||
"memo": "T-004-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2000",
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 5000000000,
|
||||
"narrative": "TAJ Allocation (M1) — HO Dr central deposits / Cr due to branch (Office 4)",
|
||||
"narrative": "Phase C HO leg — TAJ Allocation (M1) Office 4",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-005",
|
||||
"memo": "T-004-BR",
|
||||
"officeId": 4,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 5000000000,
|
||||
"narrative": "Phase C branch leg — TAJ Allocation (M1) Office 4",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-005-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2000",
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 5000000000,
|
||||
"narrative": "Aseret Allocation (M1) — HO Dr central deposits / Cr due to branch (Office 5)",
|
||||
"narrative": "Phase C HO leg — Aseret Allocation (M1) Office 5",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-006",
|
||||
"memo": "T-005-BR",
|
||||
"officeId": 5,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 5000000000,
|
||||
"narrative": "Phase C branch leg — Aseret Allocation (M1) Office 5",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-006-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2000",
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 5000000000,
|
||||
"narrative": "Mann Li Allocation (M1) — HO Dr central deposits / Cr due to branch (Office 6)",
|
||||
"narrative": "Phase C HO leg — Mann Li Allocation (M1) Office 6",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-007",
|
||||
"memo": "T-006-BR",
|
||||
"officeId": 6,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 5000000000,
|
||||
"narrative": "Phase C branch leg — Mann Li Allocation (M1) Office 6",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-007-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2000",
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 50000000000,
|
||||
"narrative": "OSJ Allocation (M1) — HO Dr central deposits / Cr due to branch (Office 7)",
|
||||
"narrative": "Phase C HO leg — OSJ Allocation (M1) Office 7",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-008",
|
||||
"memo": "T-007-BR",
|
||||
"officeId": 7,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 50000000000,
|
||||
"narrative": "Phase C branch leg — OSJ Allocation (M1) Office 7",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-008-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2000",
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 50000000000,
|
||||
"narrative": "Alltra Allocation (M1) — HO Dr central deposits / Cr due to branch (Office 8)",
|
||||
"narrative": "Phase C HO leg — Alltra Allocation (M1) Office 8",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-008-BR",
|
||||
"officeId": 8,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 50000000000,
|
||||
"narrative": "Phase C branch leg — Alltra Allocation (M1) Office 8",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
@@ -122,7 +183,7 @@
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 1000000000,
|
||||
"narrative": "HOSPITALLERS-24 opening branch leg — Dr due from HO / Cr M1 circulating",
|
||||
"narrative": "HOSPITALLERS-24 opening branch leg — Dr due from HO / Cr M1 circulating (live $1B post)",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
@@ -182,7 +243,7 @@
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 0,
|
||||
"narrative": "SBLC trade finance — Dr inter-office / contingent receivable / Cr M1",
|
||||
"narrative": "SBLC trade finance — Dr inter-office receivable / Cr M1",
|
||||
"ipsasRef": "IPSAS 28, 29",
|
||||
"moneyLayer": "M4"
|
||||
},
|
||||
@@ -222,7 +283,7 @@
|
||||
"debitGlCode": "13010",
|
||||
"creditGlCode": "2000",
|
||||
"amount": 0,
|
||||
"narrative": "Nostro settlement — Dr FX nostro asset / Cr M1 central deposits",
|
||||
"narrative": "Nostro settlement — Dr FX nostro asset / Cr M1 central pool",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"description": "HOSPITALLERS Office 24 — $1B M1 opening (two-leg). Branch leg posted live in Fineract; HO leg per Phase C.",
|
||||
"source": "config/offices/office-24-opening-journal.v1.json",
|
||||
"currencyCode": "USD",
|
||||
"officeId": 24,
|
||||
"entries": [
|
||||
{
|
||||
"memo": "T-H24-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 1000000000,
|
||||
"narrative": "HOSPITALLERS-24 opening HO leg",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-H24-BR",
|
||||
"officeId": 24,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 1000000000,
|
||||
"narrative": "HOSPITALLERS-24 opening branch leg (live post)",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"description": "Phase C interoffice two-leg pattern (canonical). HO: 2100→2410. Branch: 1410→2100. Mirrors live HOSPITALLERS-24 $1B opening.",
|
||||
"source": "config/omnl-journal-matrix.json",
|
||||
"currencyCode": "USD",
|
||||
"entries": [
|
||||
{
|
||||
"memo": "T-002A-HO",
|
||||
"officeId": 1,
|
||||
"debitGlCode": "2100",
|
||||
"creditGlCode": "2410",
|
||||
"amount": 0,
|
||||
"narrative": "Phase C HO leg template — Dr M1 circulating / Cr due to branch",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
},
|
||||
{
|
||||
"memo": "T-002A-BR",
|
||||
"officeId": 2,
|
||||
"debitGlCode": "1410",
|
||||
"creditGlCode": "2100",
|
||||
"amount": 0,
|
||||
"narrative": "Phase C branch leg template — Dr due from HO / Cr M1 circulating",
|
||||
"ipsasRef": "IPSAS 28",
|
||||
"moneyLayer": "M1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,9 @@ Endpoints are served under **`/api/v1`** when the token-aggregation service is r
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| [`config/omnl-ipsas-gl-registry.json`](../../config/omnl-ipsas-gl-registry.json) | Canonical GL codes (1000, 1050, 2000, 2100, 3000), IPSAS references, allowed pairs, monetary-layer hints |
|
||||
| [`config/omnl-journal-matrix.json`](../../config/omnl-journal-matrix.json) | T-001…T-008 journal lines |
|
||||
| [`config/omnl-journal-matrix.json`](../../config/omnl-journal-matrix.json) | IPSAS double-entry matrix (Phase C interoffice, M0–M4, Office 24 opening) |
|
||||
| [`docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix-phase-c-interoffice.json`](../../docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix-phase-c-interoffice.json) | Phase C HO/branch template |
|
||||
| [`docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix-hospitallers24-opening-1b.json`](../../docs/04-configuration/mifos-omnl-central-bank/omnl-journal-matrix-hospitallers24-opening-1b.json) | HOSPITALLERS-24 $1B opening two-leg reference |
|
||||
|
||||
Override paths with `OMNL_IPSAS_GL_REGISTRY` and `OMNL_JOURNAL_MATRIX_PATH` if needed.
|
||||
|
||||
@@ -64,4 +66,25 @@ Content-Type: application/json
|
||||
{"pairs":[{"debitGlCode":"1050","creditGlCode":"2000"}]}
|
||||
```
|
||||
|
||||
Expect `ipsasCompliantPair: true` when the pair is allowed in the registry or matrix.
|
||||
Expect `ipsasCompliantPair: true` when the pair is allowed in the registry or matrix. Same-GL pairs (e.g. `2000/2000`) return `doubleEntryValid: false`, `rejectReason: "same_gl_account"`.
|
||||
|
||||
### Post-deploy validation (token-aggregation default port 3000)
|
||||
|
||||
```bash
|
||||
# FAIL — same GL (invalid double entry)
|
||||
curl -s "http://127.0.0.1:3000/api/v1/omnl/ipsas/validate-pair?debitGlCode=2000&creditGlCode=2000"
|
||||
|
||||
# PASS — Office 24 branch leg (live $1B pattern)
|
||||
curl -s "http://127.0.0.1:3000/api/v1/omnl/ipsas/validate-pair?debitGlCode=1410&creditGlCode=2100"
|
||||
|
||||
# PASS — Phase C HO interoffice leg
|
||||
curl -s "http://127.0.0.1:3000/api/v1/omnl/ipsas/validate-pair?debitGlCode=2100&creditGlCode=2410"
|
||||
|
||||
# PASS — M2→M3 token load
|
||||
curl -s "http://127.0.0.1:3000/api/v1/omnl/ipsas/validate-pair?debitGlCode=2200&creditGlCode=2300"
|
||||
|
||||
# Reconcile anchor changes when registry/matrix updates
|
||||
curl -s "http://127.0.0.1:3000/api/v1/omnl/reconcile-anchor"
|
||||
```
|
||||
|
||||
**Phase C rule:** HO allocation uses **`2100 → 2410`** (not `2000 → 2410`). Branch leg **`1410 → 2100`**. GL `2100` is branch spendable M1 (matches Office 24 settlement profile `glM1`).
|
||||
|
||||
@@ -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