import { v4 as uuidv4 } from 'uuid'; import { buildMoneySupplySnapshot, GL_CODES, isIbanValid, m2FiatToTokenLoadAmount, m3TokenLoadAmount, resolveInstrumentGl, resolveM4OutboundJournal, rollExternalTransferVerbiage, swiftKindForRequest, type ExternalTransferRequest, type SettlementRecord, } from '@dbis/settlement-core'; import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config'; import { fetchGlBalances, postJournalEntry, resolveGlAccountId } from '../adapters/fineract'; import { HybxProductionRail } from '../adapters/hybx-production'; import { archiveIso20022, requestTokenMint } from '../adapters/omnl'; import { buildMt102Stub, buildMt103Stub, forwardSwiftToListener } from '../adapters/swift'; import { settlementStore } from '../store/settlement-store'; export async function processExternalTransfer( input: ExternalTransferRequest, ): Promise { const cfg = loadConfig(); const profile = loadOfficeProfile(); const officeId = settlementOfficeId(input.officeId); const req: ExternalTransferRequest = { ...input, officeId }; const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey); if (existing?.phase === 'SETTLED') return existing; const now = new Date().toISOString(); let record: SettlementRecord = existing ?? { settlementId: uuidv4(), idempotencyKey: req.idempotencyKey, phase: 'RECEIVED', request: req, errors: [], createdAt: now, updatedAt: now, }; const advance = (phase: SettlementRecord['phase'], patch: Partial = {}) => { record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; settlementStore.save(record); }; try { if (!isIbanValid(req.creditorIban)) { throw new Error(`Invalid creditor IBAN: ${req.creditorIban}`); } advance('VALIDATED'); const verbiage = rollExternalTransferVerbiage(req); advance('VERBIAGE_ROLLED', { verbiageDocument: verbiage }); const amount = parseFloat(req.amount) || 0; const tf = req.tradeFinance; const gl = tf ? resolveInstrumentGl(tf.instrument) : null; 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([ resolveGlAccountId(debitGl), resolveGlAccountId(creditGl), ]); const je = await postJournalEntry({ officeId: req.officeId, transactionDate: req.valueDate, referenceNumber: req.idempotencyKey, comments: verbiage.slice(0, 500), debits: [{ glAccountId: debitGlId, amount }], credits: [{ glAccountId: creditGlId, amount }], }); advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); } else { advance('FINERACT_POSTED'); } if (cfg.rails.hybx.enabled && req.rail !== 'INTERNAL') { const hybx = new HybxProductionRail(); const pay = await hybx.dispatchPayment({ idempotencyKey: req.idempotencyKey, amount: req.amount, currency: req.currency, creditorIban: req.creditorIban, creditorBic: req.creditorBic, beneficiaryName: req.beneficiaryName, remittanceInfo: req.remittanceInfo, rail: req.rail, }); advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId }); } else { advance('HYBX_RAIL_DISPATCHED'); } const swiftKind = swiftKindForRequest(req); const swiftRaw = swiftKind === 'MT102' ? buildMt102Stub({ senderRef: req.idempotencyKey, currency: req.currency, amount: req.amount, valueDate: req.valueDate, orderingInstitution: req.orderingName ?? cfg.omnlLegalName, beneficiary: req.beneficiaryName, iban: req.creditorIban, remittance: req.remittanceInfo, }) : buildMt103Stub({ senderRef: req.idempotencyKey, currency: req.currency, amount: req.amount, valueDate: req.valueDate, orderingCustomer: req.orderingName ?? cfg.omnlLegalName, beneficiary: req.beneficiaryName, iban: req.creditorIban, bic: req.creditorBic, remittance: req.remittanceInfo, }); if (cfg.rails.swift.enabled) { await forwardSwiftToListener(swiftRaw); } const iso = await archiveIso20022({ messageType: swiftKind === 'MT102' ? 'pacs.008' : 'pacs.008', direction: 'OUTBOUND', raw: swiftRaw, settlementRef: record.settlementId, }); advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId }); if (req.convertToCrypto?.enabled) { const balances = await fetchGlBalances(req.officeId); const snap = buildMoneySupplySnapshot(req.officeId, balances); const { loadAmount, fullyBacked } = m2FiatToTokenLoadAmount( req.amount, snap.M2.broadMoney, ); const m3Check = m3TokenLoadAmount(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities); if (!fullyBacked || !m3Check.fullyBacked) { record.errors.push( `M2/M3 backing insufficient for token load (${loadAmount}/${req.amount}, M3 headroom ${m3Check.loadAmount})`, ); advance('FAILED'); return record; } const mint = await requestTokenMint({ lineId: req.convertToCrypto.tokenLineId, amount: m3Check.loadAmount, recipient: req.convertToCrypto.recipientAddress, settlementRef: record.settlementId, dryRun: !cfg.production.allowChainMintExecute, symbol: req.convertToCrypto.symbol, tokenAddress: req.convertToCrypto.tokenAddress, }); advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); } else { advance('CHAIN_MINT_REQUESTED'); } advance('SETTLED'); return record; } catch (err) { const msg = err instanceof Error ? err.message : String(err); record.errors.push(msg); advance('FAILED'); return record; } } export async function getMoneySupply(officeId: number) { const balances = await fetchGlBalances(officeId); return buildMoneySupplySnapshot(officeId, balances); } export function getSettlementStatus(id: string): SettlementRecord | null { return settlementStore.getById(id); }