Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
/**
|
|
* camt.025 (Receipt) and camt.054 (Bank-to-Customer Debit/Credit
|
|
* Notification) ingestion.
|
|
*
|
|
* Arch §4.3 + §9.2. These are the inbound settlement-confirmation
|
|
* messages that allow the VALIDATING phase to mark the payment leg
|
|
* as SETTLED. The parser is intentionally minimal — just enough to
|
|
* extract the fields the VALIDATING reconciliation compares against.
|
|
*/
|
|
|
|
export interface Camt025Receipt {
|
|
type: "camt.025";
|
|
messageId: string;
|
|
originalMessageId: string;
|
|
status: "ACCP" | "ACSC" | "ACSP" | "RJCT" | "PDNG" | string;
|
|
reasonCode?: string;
|
|
dateTime?: string;
|
|
}
|
|
|
|
export interface Camt054Notification {
|
|
type: "camt.054";
|
|
messageId: string;
|
|
creditDebitIndicator: "CRDT" | "DBIT";
|
|
amount: number;
|
|
currency: string;
|
|
endToEndId?: string;
|
|
valueDate?: string;
|
|
bookingDate?: string;
|
|
}
|
|
|
|
export type CamtMessage = Camt025Receipt | Camt054Notification;
|
|
|
|
function extractTag(xml: string, tag: string): string | undefined {
|
|
const re = new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`);
|
|
const m = re.exec(xml);
|
|
return m ? m[1].trim() : undefined;
|
|
}
|
|
|
|
function extractAmountWithCcy(xml: string, tag: string): { amount: number; currency: string } | undefined {
|
|
const re = new RegExp(`<${tag}[^>]*Ccy="([A-Z]{3})"[^>]*>([^<]*)</${tag}>`);
|
|
const m = re.exec(xml);
|
|
return m ? { currency: m[1], amount: Number(m[2]) } : undefined;
|
|
}
|
|
|
|
/**
|
|
* Parse a camt.025 Receipt. Only fields used by the orchestrator are
|
|
* surfaced; everything else stays in the raw XML.
|
|
*/
|
|
export function parseCamt025(xml: string): Camt025Receipt {
|
|
if (!/xmlns="urn:iso:std:iso:20022:tech:xsd:camt\.025/.test(xml)) {
|
|
throw new Error("camt.025: xmlns marker not found");
|
|
}
|
|
const messageId = extractTag(xml, "MsgId") ?? "";
|
|
const originalMessageId = extractTag(xml, "OrgnlMsgId") ?? "";
|
|
const status = (extractTag(xml, "Cd") ?? extractTag(xml, "ConfSts") ?? "PDNG") as Camt025Receipt["status"];
|
|
const reasonCode = extractTag(xml, "PrtryStsRsn") ?? extractTag(xml, "Rsn");
|
|
const dateTime = extractTag(xml, "CreDtTm");
|
|
if (!messageId) throw new Error("camt.025: missing MsgId");
|
|
if (!originalMessageId) throw new Error("camt.025: missing OrgnlMsgId");
|
|
return { type: "camt.025", messageId, originalMessageId, status, reasonCode, dateTime };
|
|
}
|
|
|
|
/**
|
|
* Parse a camt.054 Credit/Debit Notification.
|
|
*/
|
|
export function parseCamt054(xml: string): Camt054Notification {
|
|
if (!/xmlns="urn:iso:std:iso:20022:tech:xsd:camt\.054/.test(xml)) {
|
|
throw new Error("camt.054: xmlns marker not found");
|
|
}
|
|
const messageId = extractTag(xml, "MsgId") ?? "";
|
|
const cdtDbt = (extractTag(xml, "CdtDbtInd") ?? "CRDT") as "CRDT" | "DBIT";
|
|
const amt = extractAmountWithCcy(xml, "Amt");
|
|
if (!amt) throw new Error("camt.054: missing Amt");
|
|
const endToEndId = extractTag(xml, "EndToEndId");
|
|
const valueDate = extractTag(xml, "ValDt");
|
|
const bookingDate = extractTag(xml, "BookgDt");
|
|
if (!messageId) throw new Error("camt.054: missing MsgId");
|
|
return {
|
|
type: "camt.054",
|
|
messageId,
|
|
creditDebitIndicator: cdtDbt,
|
|
amount: amt.amount,
|
|
currency: amt.currency,
|
|
endToEndId,
|
|
valueDate,
|
|
bookingDate,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Dispatch on the xmlns marker. Throws if the document is neither
|
|
* camt.025 nor camt.054.
|
|
*/
|
|
export function parseCamt(xml: string): CamtMessage {
|
|
if (/xmlns="urn:iso:std:iso:20022:tech:xsd:camt\.025/.test(xml)) return parseCamt025(xml);
|
|
if (/xmlns="urn:iso:std:iso:20022:tech:xsd:camt\.054/.test(xml)) return parseCamt054(xml);
|
|
throw new Error("camt: unsupported or missing xmlns (expected camt.025 or camt.054)");
|
|
}
|
|
|
|
/**
|
|
* Reconcile a camt.054 credit notification against an expected
|
|
* (amount, currency, endToEndId). Returns the list of mismatches so
|
|
* VALIDATING can feed them into Data.valueMismatch().
|
|
*/
|
|
export interface ReconcileExpected {
|
|
amount: number;
|
|
currency: string;
|
|
endToEndId?: string;
|
|
}
|
|
|
|
export function reconcileCamt054(
|
|
msg: Camt054Notification,
|
|
expected: ReconcileExpected,
|
|
): Array<{ field: string; expected: unknown; actual: unknown }> {
|
|
const mismatches: Array<{ field: string; expected: unknown; actual: unknown }> = [];
|
|
if (msg.creditDebitIndicator !== "CRDT") {
|
|
mismatches.push({ field: "creditDebitIndicator", expected: "CRDT", actual: msg.creditDebitIndicator });
|
|
}
|
|
if (msg.currency !== expected.currency) {
|
|
mismatches.push({ field: "currency", expected: expected.currency, actual: msg.currency });
|
|
}
|
|
if (msg.amount !== expected.amount) {
|
|
mismatches.push({ field: "amount", expected: expected.amount, actual: msg.amount });
|
|
}
|
|
if (expected.endToEndId && msg.endToEndId && msg.endToEndId !== expected.endToEndId) {
|
|
mismatches.push({ field: "endToEndId", expected: expected.endToEndId, actual: msg.endToEndId });
|
|
}
|
|
return mismatches;
|
|
}
|