Files
CurrenciCombo/orchestrator/src/services/swift/mt202.ts
Devin 632f309ffc
Some checks failed
CI / Frontend Lint (pull_request) Failing after 5s
CI / Frontend Type Check (pull_request) Failing after 7s
CI / Frontend Build (pull_request) Failing after 6s
CI / Frontend E2E Tests (pull_request) Failing after 8s
CI / Orchestrator Build (pull_request) Failing after 6s
CI / Contracts Compile (pull_request) Failing after 7s
CI / Contracts Test (pull_request) Failing after 6s
Code Quality / SonarQube Analysis (pull_request) Failing after 19s
Code Quality / Code Quality Checks (pull_request) Failing after 5s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 5s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 4s
PR E: SWIFT gateway (MT760, pacs.009, MT202, camt.025/054) — arch step 6
Outbound generators:
- swift/mt760.ts: SBLC issuance (FIN Cat-7). 12-tag message built from
  InstrumentTerms with deterministic messageHash() for planHash
  anchoring. URDG 758 / UCP 600 aware.
- swift/pacs009.ts: FI-to-FI credit transfer (ISO 20022 XML,
  pacs.009.001.08). Fixes the pacs.008 mis-routing flagged in the
  gap-analysis (pacs.008 is customer-to-bank; pacs.009 is bank-to-bank).
  BIC validation on all four agents.
- swift/mt202.ts: FIN equivalent of pacs.009 for non-migrated corridors.
  32A amount formatted with SWIFT decimal comma.

Inbound parsers:
- swift/camt.ts: parseCamt025 (receipt / status), parseCamt054
  (credit/debit notification), reconcileCamt054 (diffs amount, ccy,
  direction, endToEndId so VALIDATING can feed mismatches into
  Data.valueMismatch()), parseCamt dispatcher on xmlns marker.

Public surface in swift/index.ts documents channel selection:
pacs.008 stays on the PSP customer leg; pacs.009/MT202 is the
interbank leg; COMMIT requires camt.025 ACSC or camt.054 CRDT
(arch §9.2 accepted !== settled).

Tests: swift.test.ts — 14 cases covering the happy path, validation
errors (bad BIC, malformed date, negative amount, missing pay step),
determinism of messageHash, camt parser + reconciliation.

tsc clean. 74 tests pass across 6 suites.
2026-04-22 17:17:19 +00:00

79 lines
2.6 KiB
TypeScript

/**
* MT202 COV — General Financial Institution Transfer (cover method).
*
* Arch §4.3. FIN equivalent of pacs.009 used on SWIFT networks that
* have not yet migrated to ISO 20022. Generated alongside pacs.009
* during transitional period — settlement confirmation can arrive on
* either channel.
*/
import type { Plan, PlanStep } from "../../types/plan";
export interface Mt202Options {
transactionReference: string;
relatedReference?: string;
valueDate: string; // YYYY-MM-DD
sendingInstitution: string; // BIC
receivingInstitution: string;// BIC
beneficiaryInstitution: string; // BIC
orderingInstitution?: string;// BIC
}
export interface Mt202Message {
sender: string;
receiver: string;
fin: string;
fields: Record<string, string>;
}
function yyMMdd(iso: string): string {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
if (!m) throw new Error(`MT202: valueDate must be YYYY-MM-DD, got '${iso}'`);
return `${m[1].slice(2)}${m[2]}${m[3]}`;
}
function bicCheck(bic: string, field: string): void {
if (!/^[A-Z0-9]{8}([A-Z0-9]{3})?$/.test(bic)) {
throw new Error(`MT202: ${field} must be a valid BIC, got '${bic}'`);
}
}
function findPayStep(plan: Plan): PlanStep {
const step = plan.steps.find((s) => s.type === "pay");
if (!step) throw new Error("MT202: plan must contain a 'pay' step");
return step;
}
export function generateMt202(plan: Plan, opts: Mt202Options): Mt202Message {
bicCheck(opts.sendingInstitution, "sendingInstitution");
bicCheck(opts.receivingInstitution, "receivingInstitution");
bicCheck(opts.beneficiaryInstitution, "beneficiaryInstitution");
if (opts.orderingInstitution) bicCheck(opts.orderingInstitution, "orderingInstitution");
const payStep = findPayStep(plan);
const ccy = (payStep.asset ?? "USD").toUpperCase();
const amount = payStep.amount.toFixed(2).replace(".", ",");
const field32A = `${yyMMdd(opts.valueDate)}${ccy}${amount}`;
const fields: Record<string, string> = {
"20": opts.transactionReference,
"21": opts.relatedReference ?? opts.transactionReference,
"32A": field32A,
"52A": opts.orderingInstitution ?? opts.sendingInstitution,
"57A": opts.receivingInstitution,
"58A": opts.beneficiaryInstitution,
};
const block1 = `{1:F01${opts.sendingInstitution.padEnd(12, "X")}0000000000}`;
const block2 = `{2:I202${opts.receivingInstitution.padEnd(12, "X")}N}`;
const block4 = Object.entries(fields).map(([t, v]) => `:${t}:${v}`).join("\n");
const block4Wrapped = `{4:\n${block4}\n-}`;
return {
sender: opts.sendingInstitution,
receiver: opts.receivingInstitution,
fin: `${block1}${block2}${block4Wrapped}`,
fields,
};
}