feat: OMNL Bank online production — 128 chains, Central Bank UI, settlement stack
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m22s
CI/CD Pipeline / Security Scanning (push) Successful in 2m30s
CI/CD Pipeline / Lint and Format (push) Failing after 44s
CI/CD Pipeline / Terraform Validation (push) Failing after 27s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 31s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 56s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 39s
Validation / validate-kubernetes (push) Failing after 14s
Validation / validate-smart-contracts (push) Failing after 20s
Validation / validate-security (push) Failing after 1m19s
Validation / validate-documentation (push) Failing after 27s
Verify Deployment / Verify Deployment (push) Failing after 1m13s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m22s
CI/CD Pipeline / Security Scanning (push) Successful in 2m30s
CI/CD Pipeline / Lint and Format (push) Failing after 44s
CI/CD Pipeline / Terraform Validation (push) Failing after 27s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 31s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 56s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 29s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 39s
Validation / validate-kubernetes (push) Failing after 14s
Validation / validate-smart-contracts (push) Failing after 20s
Validation / validate-security (push) Failing after 1m19s
Validation / validate-documentation (push) Failing after 27s
Verify Deployment / Verify Deployment (push) Failing after 1m13s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
buildMoneySupplySnapshot,
|
||||
isIbanValid,
|
||||
m2FiatToTokenLoadAmount,
|
||||
resolveInstrumentGl,
|
||||
rollExternalTransferVerbiage,
|
||||
swiftKindForRequest,
|
||||
type ExternalTransferRequest,
|
||||
type SettlementRecord,
|
||||
} from '@dbis/settlement-core';
|
||||
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
|
||||
import { fetchGlBalances, postJournalEntry } 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';
|
||||
|
||||
function glId(code: string): number {
|
||||
return parseInt(code, 10);
|
||||
}
|
||||
|
||||
export async function processExternalTransfer(
|
||||
input: ExternalTransferRequest,
|
||||
): Promise<SettlementRecord> {
|
||||
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<SettlementRecord> = {}) => {
|
||||
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 debitGl = gl?.debitGl ?? profile.settlement.glSettlement ?? cfg.moneySupply.glSettlement;
|
||||
const creditGl = gl?.creditGl ?? profile.settlement.glM1 ?? cfg.moneySupply.glM1;
|
||||
|
||||
if (amount > 0) {
|
||||
const je = await postJournalEntry({
|
||||
officeId: req.officeId,
|
||||
transactionDate: req.valueDate,
|
||||
referenceNumber: req.idempotencyKey,
|
||||
comments: verbiage.slice(0, 500),
|
||||
debits: [{ glAccountId: glId(debitGl), amount }],
|
||||
credits: [{ glAccountId: glId(creditGl), 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,
|
||||
);
|
||||
if (!fullyBacked) {
|
||||
record.errors.push(`M2 backing insufficient for token load (${loadAmount}/${req.amount})`);
|
||||
}
|
||||
const mint = await requestTokenMint({
|
||||
lineId: req.convertToCrypto.tokenLineId,
|
||||
amount: loadAmount,
|
||||
recipient: req.convertToCrypto.recipientAddress,
|
||||
settlementRef: record.settlementId,
|
||||
dryRun: !cfg.production.allowChainMintExecute,
|
||||
});
|
||||
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);
|
||||
}
|
||||
33
services/settlement-middleware/src/workflows/office-scope.ts
Normal file
33
services/settlement-middleware/src/workflows/office-scope.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { ExternalTransferRequest } from '@dbis/settlement-core';
|
||||
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
|
||||
|
||||
/** Bind every settlement request to Office 24 (same profile as OMNL terminal). */
|
||||
export function bindOffice24(req: Partial<ExternalTransferRequest>): ExternalTransferRequest {
|
||||
const cfg = loadConfig();
|
||||
const profile = loadOfficeProfile();
|
||||
const officeId = settlementOfficeId(req.officeId);
|
||||
|
||||
if (!req.idempotencyKey || !req.creditorIban || !req.amount) {
|
||||
throw new Error('idempotencyKey, creditorIban, amount required');
|
||||
}
|
||||
|
||||
return {
|
||||
officeId,
|
||||
valueDate: req.valueDate ?? new Date().toISOString().slice(0, 10),
|
||||
currency: req.currency ?? profile.settlement.defaultCurrency ?? cfg.defaultCurrency,
|
||||
moneyLayers: (req.moneyLayers ?? profile.settlement.moneyLayers) as ExternalTransferRequest['moneyLayers'],
|
||||
rail: req.rail ?? 'SWIFT',
|
||||
remittanceInfo: req.remittanceInfo ?? `OMNL Office 24 settlement — ${profile.externalId}`,
|
||||
beneficiaryName: req.beneficiaryName ?? 'Beneficiary',
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
creditorIban: req.creditorIban,
|
||||
amount: req.amount,
|
||||
debtorIban: req.debtorIban,
|
||||
creditorBic: req.creditorBic,
|
||||
orderingName: req.orderingName ?? profile.omnlLegalName,
|
||||
swiftMessageKind: req.swiftMessageKind,
|
||||
tradeFinance: req.tradeFinance,
|
||||
rwaVerbiage: req.rwaVerbiage,
|
||||
convertToCrypto: req.convertToCrypto,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { ExternalTransferRequest, SettlementRecord } from '@dbis/settlement-core';
|
||||
import { forwardSwiftToListener } from '../adapters/swift';
|
||||
import { settlementOfficeId } from '../config';
|
||||
import { settlementStore } from '../store/settlement-store';
|
||||
import { processExternalTransfer } from './external-transfer';
|
||||
|
||||
/** Inbound MT103/MT102 → settlement workflow */
|
||||
export async function processSwiftInbound(raw: string): Promise<SettlementRecord> {
|
||||
await forwardSwiftToListener(raw);
|
||||
|
||||
const isMt102 = /\bMT102\b|:23:/.test(raw) || /:32B:/.test(raw);
|
||||
const refMatch = raw.match(/:20:([^\n]+)/);
|
||||
const amtMatch = raw.match(/:32[AB]:(\d{6})([A-Z]{3})([\d,]+)/);
|
||||
const ibanMatch = raw.match(/\/([A-Z]{2}\d{2}[A-Z0-9]+)/);
|
||||
const bnfMatch = raw.match(/:59:[^\n]*\n([^\n:]+)/);
|
||||
const remMatch = raw.match(/:70:([^\n]+)/);
|
||||
|
||||
const req: ExternalTransferRequest = {
|
||||
idempotencyKey: refMatch?.[1]?.trim() || `SWIFT-IN-${uuidv4()}`,
|
||||
officeId: settlementOfficeId(),
|
||||
valueDate: new Date().toISOString().slice(0, 10),
|
||||
currency: amtMatch?.[2] ?? 'USD',
|
||||
amount: (amtMatch?.[3] ?? '0').replace(',', '.'),
|
||||
creditorIban: ibanMatch?.[1] ?? '',
|
||||
beneficiaryName: bnfMatch?.[1]?.trim() ?? 'Inbound beneficiary',
|
||||
remittanceInfo: remMatch?.[1]?.trim() ?? 'Inbound SWIFT settlement',
|
||||
moneyLayers: isMt102 ? ['M1', 'M2'] : ['M0', 'M1'],
|
||||
rail: 'SWIFT',
|
||||
swiftMessageKind: isMt102 ? 'MT102' : 'MT103',
|
||||
rwaVerbiage: { assetClass: 'RWA', tokenLineId: 'USD-M1', externalTransferRoll: true },
|
||||
};
|
||||
|
||||
if (!req.creditorIban) {
|
||||
const stub: SettlementRecord = {
|
||||
settlementId: uuidv4(),
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
phase: 'RECEIVED',
|
||||
request: req,
|
||||
errors: ['Parsed inbound SWIFT — manual IBAN review required'],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
settlementStore.save(stub);
|
||||
return stub;
|
||||
}
|
||||
|
||||
return processExternalTransfer(req);
|
||||
}
|
||||
115
services/settlement-middleware/src/workflows/token-load.ts
Normal file
115
services/settlement-middleware/src/workflows/token-load.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
buildMoneySupplySnapshot,
|
||||
GL_CODES,
|
||||
m2FiatToTokenLoadAmount,
|
||||
type TokenLoadRequest,
|
||||
type SettlementRecord,
|
||||
} from '@dbis/settlement-core';
|
||||
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
|
||||
import { fetchGlBalances, postJournalEntry } from '../adapters/fineract';
|
||||
import { requestTokenMint } from '../adapters/omnl';
|
||||
import { settlementStore } from '../store/settlement-store';
|
||||
|
||||
function glId(code: string): number {
|
||||
return parseInt(code, 10);
|
||||
}
|
||||
|
||||
export async function processTokenLoad(input: TokenLoadRequest): Promise<SettlementRecord> {
|
||||
const cfg = loadConfig();
|
||||
const profile = loadOfficeProfile();
|
||||
const officeId = settlementOfficeId(input.officeId);
|
||||
const req = { ...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: {
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
officeId,
|
||||
valueDate: now.slice(0, 10),
|
||||
currency: req.currency ?? 'USD',
|
||||
amount: req.amount,
|
||||
creditorIban: 'INTERNAL-M2-TOKEN-LOAD',
|
||||
beneficiaryName: req.recipientAddress,
|
||||
remittanceInfo: `M2 token load ${req.lineId} → ${req.symbol ?? req.tokenAddress ?? 'token'}`,
|
||||
moneyLayers: ['M2'],
|
||||
rail: 'INTERNAL',
|
||||
convertToCrypto: {
|
||||
enabled: true,
|
||||
targetChainId: 138,
|
||||
tokenLineId: req.lineId,
|
||||
recipientAddress: req.recipientAddress,
|
||||
},
|
||||
},
|
||||
errors: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const advance = (phase: SettlementRecord['phase'], patch: Partial<SettlementRecord> = {}) => {
|
||||
record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch };
|
||||
settlementStore.save(record);
|
||||
};
|
||||
|
||||
try {
|
||||
advance('VALIDATED');
|
||||
advance('VERBIAGE_ROLLED', {
|
||||
verbiageDocument: [
|
||||
`OMNL M2 fiat-backed token load`,
|
||||
`Office ${officeId} · line ${req.lineId}`,
|
||||
`Amount ${req.amount} · recipient ${req.recipientAddress}`,
|
||||
`GL ${GL_CODES.M2_BROAD} → ${GL_CODES.CRYPTO_TOKEN_LIABILITY}`,
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const balances = await fetchGlBalances(officeId);
|
||||
const snap = buildMoneySupplySnapshot(officeId, balances);
|
||||
const { loadAmount, fullyBacked } = m2FiatToTokenLoadAmount(req.amount, snap.M2.broadMoney);
|
||||
if (!fullyBacked) {
|
||||
record.errors.push(`M2 backing insufficient (${loadAmount}/${req.amount})`);
|
||||
advance('FAILED');
|
||||
return record;
|
||||
}
|
||||
|
||||
const amount = parseFloat(loadAmount) || 0;
|
||||
if (amount > 0) {
|
||||
const je = await postJournalEntry({
|
||||
officeId,
|
||||
transactionDate: now.slice(0, 10),
|
||||
referenceNumber: req.idempotencyKey,
|
||||
comments: `M2 token load ${req.lineId}`,
|
||||
debits: [{ glAccountId: glId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD), amount }],
|
||||
credits: [{ glAccountId: glId(GL_CODES.CRYPTO_TOKEN_LIABILITY), amount }],
|
||||
});
|
||||
advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) });
|
||||
} else {
|
||||
advance('FINERACT_POSTED');
|
||||
}
|
||||
|
||||
advance('HYBX_RAIL_DISPATCHED');
|
||||
advance('ISO20022_ARCHIVED');
|
||||
|
||||
const mint = await requestTokenMint({
|
||||
lineId: req.lineId,
|
||||
amount: loadAmount,
|
||||
recipient: req.recipientAddress,
|
||||
settlementRef: record.settlementId,
|
||||
dryRun: !cfg.production.allowChainMintExecute,
|
||||
symbol: req.symbol,
|
||||
tokenAddress: req.tokenAddress,
|
||||
});
|
||||
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
|
||||
advance('SETTLED');
|
||||
return record;
|
||||
} catch (err) {
|
||||
record.errors.push(err instanceof Error ? err.message : String(err));
|
||||
advance('FAILED');
|
||||
return record;
|
||||
}
|
||||
}
|
||||
132
services/settlement-middleware/src/workflows/transfer.ts
Normal file
132
services/settlement-middleware/src/workflows/transfer.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { GL_CODES, type TransferRequest, type SettlementRecord } from '@dbis/settlement-core';
|
||||
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
|
||||
import { postJournalEntry } from '../adapters/fineract';
|
||||
import { HybxProductionRail } from '../adapters/hybx-production';
|
||||
import { archiveIso20022 } from '../adapters/omnl';
|
||||
import { buildMt103Stub, forwardSwiftToListener } from '../adapters/swift';
|
||||
import { settlementStore } from '../store/settlement-store';
|
||||
|
||||
function glId(code: string): number {
|
||||
return parseInt(code, 10);
|
||||
}
|
||||
|
||||
export async function processTransfer(input: TransferRequest): Promise<SettlementRecord> {
|
||||
const cfg = loadConfig();
|
||||
const profile = loadOfficeProfile();
|
||||
const officeId = settlementOfficeId(input.officeId);
|
||||
const req = { ...input, officeId };
|
||||
const isExternal = req.rail === 'SWIFT' || req.rail === 'RTGS';
|
||||
|
||||
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: {
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
officeId,
|
||||
valueDate: now.slice(0, 10),
|
||||
currency: req.currency ?? 'USD',
|
||||
amount: req.amount,
|
||||
creditorIban: req.creditorIban ?? (isExternal ? '' : 'INTERNAL-CHAIN138'),
|
||||
beneficiaryName: req.beneficiaryName ?? req.recipientAddress,
|
||||
remittanceInfo: req.remittanceInfo ?? `Transfer ${req.tokenSymbol} → ${req.recipientAddress}`,
|
||||
moneyLayers: req.moneyLayers.length ? req.moneyLayers : ['M2'],
|
||||
rail: req.rail === 'CHAIN138' ? 'CHAIN138' : req.rail === 'INTERNAL' ? 'INTERNAL' : 'SWIFT',
|
||||
},
|
||||
errors: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const advance = (phase: SettlementRecord['phase'], patch: Partial<SettlementRecord> = {}) => {
|
||||
record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch };
|
||||
settlementStore.save(record);
|
||||
};
|
||||
|
||||
try {
|
||||
if (isExternal && !req.creditorIban) {
|
||||
throw new Error('creditorIban required for external transfer');
|
||||
}
|
||||
advance('VALIDATED');
|
||||
advance('VERBIAGE_ROLLED', {
|
||||
verbiageDocument: [
|
||||
`OMNL ${req.rail} transfer · ${req.tokenSymbol}`,
|
||||
`M2 layer · ${req.amount}`,
|
||||
`To ${req.recipientAddress}`,
|
||||
isExternal ? `IBAN ${req.creditorIban}` : 'Internal / Chain 138',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
const amount = parseFloat(req.amount) || 0;
|
||||
if (amount > 0) {
|
||||
const je = await postJournalEntry({
|
||||
officeId,
|
||||
transactionDate: now.slice(0, 10),
|
||||
referenceNumber: req.idempotencyKey,
|
||||
comments: `${req.rail} transfer ${req.tokenSymbol}`,
|
||||
debits: [{ glAccountId: glId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD), amount }],
|
||||
credits: [{ glAccountId: glId(profile.settlement.glSettlement ?? GL_CODES.SETTLEMENT_SUSPENSE), amount }],
|
||||
});
|
||||
advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) });
|
||||
} else {
|
||||
advance('FINERACT_POSTED');
|
||||
}
|
||||
|
||||
if (isExternal && cfg.rails.hybx.enabled) {
|
||||
const hybx = new HybxProductionRail();
|
||||
const pay = await hybx.dispatchPayment({
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
amount: req.amount,
|
||||
currency: req.currency ?? 'USD',
|
||||
creditorIban: req.creditorIban!,
|
||||
beneficiaryName: req.beneficiaryName ?? req.recipientAddress,
|
||||
remittanceInfo: req.remittanceInfo ?? `OMNL ${req.tokenSymbol} external transfer`,
|
||||
rail: req.rail === 'RTGS' ? 'RTGS' : 'SWIFT',
|
||||
});
|
||||
advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId });
|
||||
} else {
|
||||
advance('HYBX_RAIL_DISPATCHED');
|
||||
}
|
||||
|
||||
if (isExternal && cfg.rails.swift.enabled && req.creditorIban) {
|
||||
const swiftRaw = buildMt103Stub({
|
||||
senderRef: req.idempotencyKey,
|
||||
currency: req.currency ?? 'USD',
|
||||
amount: req.amount,
|
||||
valueDate: now.slice(0, 10),
|
||||
orderingCustomer: cfg.omnlLegalName,
|
||||
beneficiary: req.beneficiaryName ?? req.recipientAddress,
|
||||
iban: req.creditorIban,
|
||||
remittance: req.remittanceInfo ?? `OMNL ${req.tokenSymbol}`,
|
||||
});
|
||||
await forwardSwiftToListener(swiftRaw);
|
||||
const iso = await archiveIso20022({
|
||||
messageType: 'pacs.008',
|
||||
direction: 'OUTBOUND',
|
||||
raw: swiftRaw,
|
||||
settlementRef: record.settlementId,
|
||||
});
|
||||
advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId });
|
||||
} else {
|
||||
advance('ISO20022_ARCHIVED');
|
||||
}
|
||||
|
||||
advance('CHAIN_MINT_REQUESTED', {
|
||||
verbiageDocument: [
|
||||
record.verbiageDocument,
|
||||
`On-chain ERC-20 transfer prepared: ${req.tokenAddress} → ${req.recipientAddress}`,
|
||||
].join('\n'),
|
||||
});
|
||||
advance('SETTLED');
|
||||
return record;
|
||||
} catch (err) {
|
||||
record.errors.push(err instanceof Error ? err.message : String(err));
|
||||
advance('FAILED');
|
||||
return record;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user