diff --git a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx index 2ccde1a..3670e34 100644 --- a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx +++ b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx @@ -136,7 +136,7 @@ export default function CentralBankDashboard() { - + @@ -238,7 +238,7 @@ export default function CentralBankDashboard() {
M3 token liability
GL {office?.settlement?.glM3 ?? '2300'}
-
M4 near money
GL {office?.settlement?.glM4NearMoney ?? '1000'} + {office?.settlement?.glM4Contingent ?? '1410'}
+
M4 near money
GL {office?.settlement?.glM4NearMoney ?? '1000'} + in-flight SWIFT
Inter-office (HO)
GL 1410
diff --git a/frontend-dapp/src/features/office24/Office24Dashboard.tsx b/frontend-dapp/src/features/office24/Office24Dashboard.tsx index 5804c2a..64e2c62 100644 --- a/frontend-dapp/src/features/office24/Office24Dashboard.tsx +++ b/frontend-dapp/src/features/office24/Office24Dashboard.tsx @@ -126,7 +126,7 @@ export default function Office24Dashboard() { - + diff --git a/packages/settlement-core/dist/money-supply.d.ts b/packages/settlement-core/dist/money-supply.d.ts index 911355e..05129d7 100644 --- a/packages/settlement-core/dist/money-supply.d.ts +++ b/packages/settlement-core/dist/money-supply.d.ts @@ -34,3 +34,12 @@ export declare function m3TokenLoadAmount(requested: string, m2Broad: string, m3 }; /** All settlement layers M0–M4 for production routing. */ export declare const ALL_MONEY_LAYERS: MoneyLayer[]; +/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */ +export declare function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: { + glM1: string; + glM2: string; + glM4NearMoney: string; +}): { + debitGl: string; + creditGl: string; +} | null; diff --git a/packages/settlement-core/dist/money-supply.js b/packages/settlement-core/dist/money-supply.js index d3d279d..eb4d31b 100644 --- a/packages/settlement-core/dist/money-supply.js +++ b/packages/settlement-core/dist/money-supply.js @@ -5,6 +5,7 @@ exports.aggregateMetaFiatLayers = aggregateMetaFiatLayers; exports.buildMoneySupplySnapshot = buildMoneySupplySnapshot; exports.m2FiatToTokenLoadAmount = m2FiatToTokenLoadAmount; exports.m3TokenLoadAmount = m3TokenLoadAmount; +exports.resolveM4OutboundJournal = resolveM4OutboundJournal; /** IPSAS GL mapping for OMNL central-bank money aggregates M0–M4 */ exports.GL_CODES = { M0_RESERVE: '1050', @@ -46,7 +47,8 @@ function buildMoneySupplySnapshot(officeId, balances) { const pending = balances[exports.GL_CODES.SETTLEMENT_SUSPENSE] ?? '0'; const contingent = balances[exports.GL_CODES.TRADE_FINANCE_CONTINGENT] ?? '0'; const inFlight = balances.inFlightSwift ?? '0'; - const m4Num = (parseFloat(pending) || 0) + (parseFloat(contingent) || 0) + (parseFloat(inFlight) || 0); + // M4 near money = GL 1000 settlement suspense + in-flight SWIFT (GL 1410 is interoffice HO receivable) + const m4Num = (parseFloat(pending) || 0) + (parseFloat(inFlight) || 0); return { asOf: new Date().toISOString(), officeId, @@ -103,3 +105,16 @@ function m3TokenLoadAmount(requested, m2Broad, m3Outstanding) { } /** All settlement layers M0–M4 for production routing. */ exports.ALL_MONEY_LAYERS = ['M0', 'M1', 'M2', 'M3', 'M4']; +/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */ +function resolveM4OutboundJournal(moneyLayers, gl) { + if (!moneyLayers.includes('M4')) + return null; + const suspense = gl.glM4NearMoney || exports.GL_CODES.SETTLEMENT_SUSPENSE; + if (moneyLayers.includes('M2')) { + return { debitGl: gl.glM2 || exports.GL_CODES.M2_BROAD, creditGl: suspense }; + } + if (moneyLayers.includes('M1')) { + return { debitGl: gl.glM1 || exports.GL_CODES.M1_CIRCULATING, creditGl: suspense }; + } + return { debitGl: exports.GL_CODES.M2_BROAD, creditGl: suspense }; +} diff --git a/packages/settlement-core/src/money-supply.ts b/packages/settlement-core/src/money-supply.ts index 9199416..2326c66 100644 --- a/packages/settlement-core/src/money-supply.ts +++ b/packages/settlement-core/src/money-supply.ts @@ -49,8 +49,8 @@ export function buildMoneySupplySnapshot( const pending = balances[GL_CODES.SETTLEMENT_SUSPENSE] ?? '0'; const contingent = balances[GL_CODES.TRADE_FINANCE_CONTINGENT] ?? '0'; const inFlight = balances.inFlightSwift ?? '0'; - const m4Num = - (parseFloat(pending) || 0) + (parseFloat(contingent) || 0) + (parseFloat(inFlight) || 0); + // M4 near money = GL 1000 settlement suspense + in-flight SWIFT (GL 1410 is interoffice HO receivable) + const m4Num = (parseFloat(pending) || 0) + (parseFloat(inFlight) || 0); return { asOf: new Date().toISOString(), officeId, @@ -116,3 +116,20 @@ export function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstand /** All settlement layers M0–M4 for production routing. */ export const ALL_MONEY_LAYERS: MoneyLayer[] = ['M0', 'M1', 'M2', 'M3', 'M4']; + +/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */ +export function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: { + glM1: string; + glM2: string; + glM4NearMoney: string; +}): { debitGl: string; creditGl: string } | null { + if (!moneyLayers.includes('M4')) return null; + const suspense = gl.glM4NearMoney || GL_CODES.SETTLEMENT_SUSPENSE; + if (moneyLayers.includes('M2')) { + return { debitGl: gl.glM2 || GL_CODES.M2_BROAD, creditGl: suspense }; + } + if (moneyLayers.includes('M1')) { + return { debitGl: gl.glM1 || GL_CODES.M1_CIRCULATING, creditGl: suspense }; + } + return { debitGl: GL_CODES.M2_BROAD, creditGl: suspense }; +} diff --git a/services/settlement-middleware/dist/workflows/external-transfer.js b/services/settlement-middleware/dist/workflows/external-transfer.js index 69fa49b..77cfc41 100644 --- a/services/settlement-middleware/dist/workflows/external-transfer.js +++ b/services/settlement-middleware/dist/workflows/external-transfer.js @@ -43,8 +43,15 @@ async function processExternalTransfer(input) { const amount = parseFloat(req.amount) || 0; const tf = req.tradeFinance; const gl = tf ? (0, settlement_core_1.resolveInstrumentGl)(tf.instrument) : null; - const debitGl = gl?.debitGl ?? profile.settlement.glSettlement ?? cfg.moneySupply.glSettlement; - const creditGl = gl?.creditGl ?? profile.settlement.glM1 ?? cfg.moneySupply.glM1; + const m4Journal = !gl + ? (0, settlement_core_1.resolveM4OutboundJournal)(req.moneyLayers, { + glM1: profile.settlement.glM1 ?? cfg.moneySupply.glM1, + glM2: profile.settlement.glM2 ?? cfg.moneySupply.glM2, + glM4NearMoney: profile.settlement.glM4NearMoney ?? cfg.moneySupply.glM4NearMoney ?? cfg.moneySupply.glSettlement, + }) + : null; + const debitGl = gl?.debitGl ?? m4Journal?.debitGl ?? profile.settlement.glM2 ?? cfg.moneySupply.glM2; + const creditGl = gl?.creditGl ?? m4Journal?.creditGl ?? profile.settlement.glM4NearMoney ?? cfg.moneySupply.glSettlement; if (amount > 0) { const [debitGlId, creditGlId] = await Promise.all([ (0, fineract_1.resolveGlAccountId)(debitGl), diff --git a/services/settlement-middleware/dist/workflows/transfer.js b/services/settlement-middleware/dist/workflows/transfer.js index cf8f7d0..1984db5 100644 --- a/services/settlement-middleware/dist/workflows/transfer.js +++ b/services/settlement-middleware/dist/workflows/transfer.js @@ -56,15 +56,29 @@ async function processTransfer(input) { verbiageDocument: [ `OMNL ${req.rail} transfer · ${req.tokenSymbol}`, `Layers ${record.request.moneyLayers.join('/')} · ${req.amount}`, - `To ${req.recipientAddress}`, - isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138', + isExternal + ? `IBAN ${req.creditorIban} · M4 GL ${profile.settlement.glM4NearMoney ?? '1000'} suspense` + : `To ${req.recipientAddress} · internal chain`, ].join('\n'), }); const amount = parseFloat(req.amount) || 0; if (amount > 0) { + const layers = record.request.moneyLayers; + const m4Journal = isExternal + ? (0, settlement_core_1.resolveM4OutboundJournal)(layers, { + glM1: profile.settlement.glM1 ?? settlement_core_1.GL_CODES.M1_CIRCULATING, + glM2: profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD, + glM4NearMoney: profile.settlement.glM4NearMoney ?? profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE, + }) + : null; + const debitGl = m4Journal?.debitGl ?? profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD; + const creditGl = m4Journal?.creditGl + ?? (isExternal + ? (profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE) + : (profile.settlement.glM3 ?? settlement_core_1.GL_CODES.M3_TOKEN_LIABILITY)); const [debitGlId, creditGlId] = await Promise.all([ - (0, fineract_1.resolveGlAccountId)(profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD), - (0, fineract_1.resolveGlAccountId)(profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE), + (0, fineract_1.resolveGlAccountId)(debitGl), + (0, fineract_1.resolveGlAccountId)(creditGl), ]); const je = await (0, fineract_1.postJournalEntry)({ officeId, diff --git a/services/settlement-middleware/src/workflows/external-transfer.ts b/services/settlement-middleware/src/workflows/external-transfer.ts index b952249..dd5f437 100644 --- a/services/settlement-middleware/src/workflows/external-transfer.ts +++ b/services/settlement-middleware/src/workflows/external-transfer.ts @@ -1,10 +1,12 @@ import { v4 as uuidv4 } from 'uuid'; import { buildMoneySupplySnapshot, + GL_CODES, isIbanValid, m2FiatToTokenLoadAmount, m3TokenLoadAmount, resolveInstrumentGl, + resolveM4OutboundJournal, rollExternalTransferVerbiage, swiftKindForRequest, type ExternalTransferRequest, @@ -56,8 +58,15 @@ export async function processExternalTransfer( const amount = parseFloat(req.amount) || 0; const tf = req.tradeFinance; const gl = tf ? resolveInstrumentGl(tf.instrument) : null; - const debitGl = gl?.debitGl ?? profile.settlement.glSettlement ?? cfg.moneySupply.glSettlement; - const creditGl = gl?.creditGl ?? profile.settlement.glM1 ?? cfg.moneySupply.glM1; + const m4Journal = !gl + ? resolveM4OutboundJournal(req.moneyLayers, { + glM1: profile.settlement.glM1 ?? cfg.moneySupply.glM1, + glM2: profile.settlement.glM2 ?? cfg.moneySupply.glM2, + glM4NearMoney: profile.settlement.glM4NearMoney ?? cfg.moneySupply.glM4NearMoney ?? cfg.moneySupply.glSettlement, + }) + : null; + const debitGl = gl?.debitGl ?? m4Journal?.debitGl ?? profile.settlement.glM2 ?? cfg.moneySupply.glM2; + const creditGl = gl?.creditGl ?? m4Journal?.creditGl ?? profile.settlement.glM4NearMoney ?? cfg.moneySupply.glSettlement; if (amount > 0) { const [debitGlId, creditGlId] = await Promise.all([ diff --git a/services/settlement-middleware/src/workflows/transfer.ts b/services/settlement-middleware/src/workflows/transfer.ts index d370e91..2025e64 100644 --- a/services/settlement-middleware/src/workflows/transfer.ts +++ b/services/settlement-middleware/src/workflows/transfer.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { GL_CODES, type TransferRequest, type SettlementRecord } from '@dbis/settlement-core'; +import { GL_CODES, resolveM4OutboundJournal, type TransferRequest, type SettlementRecord } from '@dbis/settlement-core'; import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config'; import { postJournalEntry, resolveGlAccountId } from '../adapters/fineract'; import { HybxProductionRail } from '../adapters/hybx-production'; @@ -57,16 +57,30 @@ export async function processTransfer(input: TransferRequest): Promise 0) { + const layers = record.request.moneyLayers; + const m4Journal = isExternal + ? resolveM4OutboundJournal(layers, { + glM1: profile.settlement.glM1 ?? GL_CODES.M1_CIRCULATING, + glM2: profile.settlement.glM2 ?? GL_CODES.M2_BROAD, + glM4NearMoney: profile.settlement.glM4NearMoney ?? profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE, + }) + : null; + const debitGl = m4Journal?.debitGl ?? profile.settlement.glM2 ?? GL_CODES.M2_BROAD; + const creditGl = m4Journal?.creditGl + ?? (isExternal + ? (profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE) + : (profile.settlement.glM3 ?? GL_CODES.M3_TOKEN_LIABILITY)); const [debitGlId, creditGlId] = await Promise.all([ - resolveGlAccountId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD), - resolveGlAccountId(profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE), + resolveGlAccountId(debitGl), + resolveGlAccountId(creditGl), ]); const je = await postJournalEntry({ officeId,