From 9dddda7e62feadfe1604f08f6c4440ce9ebc99a7 Mon Sep 17 00:00:00 2001 From: Sina Ahmadi <14019938+tanacosa@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:07:42 +0200 Subject: [PATCH] Add HKCCS, HKVPP and HKIPZ SEPA credit transfer segments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registers HKCCS (Einreichung SEPA-Überweisung), its HKVPP pain.002 descriptor prefix, and HKIPZ (SEPA Instant / SCT Inst) as first-class built-in segments, so consumers can issue SEPA wire transfers without patching node_modules or reaching into dist/segments/registry.js at runtime. The deep-import workaround crashes under Deno's npm: resolver because the package's exports field does not expose the registry subpath, which blocks every Deno-based consumer (edge functions, Deno Deploy, Cloudflare Workers with Node compat). Registering the segments in src/segments/registry.ts gives every consumer the writable payment surface by default. docs/HKCCS.md captures the Sparkasse Berlin protocol quirks observed during live validation: the ReqdExctnDt=1999-01-01 sentinel, the HKVPP-before-HKCCS ordering requirement, pushTAN method 923, and the response-code signatures. All 112 existing tests pass; tsc build is clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/HKCCS.md | 114 +++++++++++++++++++++++++++++++++++++++ src/segments/HKCCS.ts | 44 +++++++++++++++ src/segments/HKIPZ.ts | 64 ++++++++++++++++++++++ src/segments/HKVPP.ts | 25 +++++++++ src/segments/registry.ts | 6 +++ 5 files changed, 253 insertions(+) create mode 100644 docs/HKCCS.md create mode 100644 src/segments/HKCCS.ts create mode 100644 src/segments/HKIPZ.ts create mode 100644 src/segments/HKVPP.ts diff --git a/docs/HKCCS.md b/docs/HKCCS.md new file mode 100644 index 0000000..289d131 --- /dev/null +++ b/docs/HKCCS.md @@ -0,0 +1,114 @@ +# HKCCS / HKIPZ — SEPA Credit Transfer + +## What this is + +`HKCCS` (Einreichung SEPA-Überweisung) is the FinTS 3.0 segment used to +initiate a single SEPA wire transfer. Together with its companion segment +`HKVPP` it forms the minimum writable payment surface of FinTS PIN/TAN. +`HKIPZ` is the SEPA Instant ("Echtzeitüberweisung", SCT Inst) variant. + +`lib-fints` historically focused on read operations (balance, statements, +securities) and did not register these segments. They are now first-class +built-ins so consumers can issue transfers without patching `node_modules` +or doing deep `registry.set()` tricks at runtime. + +## Why these are built-ins + +Runtime registration against `registry.js` works in Node.js but breaks in +Deno: the `npm:` resolver enforces the package's `exports` field, and +without a wildcard subpath the deep import `lib-fints/dist/segments/registry.js` +is blocked. Every Deno-based consumer (edge functions, Deno Deploy, +Cloudflare Workers with Node compat) hits this the moment it tries to add +HKCCS. Baking the segments in removes the failure mode. + +## Usage + +```ts +import { Dialog, FinTSClient, FinTSConfig } from 'lib-fints'; +import { CustomerOrderInteraction } from 'lib-fints/dist/interactions/customerInteraction.js'; + +class TransferInteraction extends CustomerOrderInteraction { + constructor( + private senderIban: string, + private senderBic: string, + private painXml: string, + private bankResponses: { code: number; text: string }[], + ) { + super('HKCCS', 'HIRMS'); + } + + createSegments(cfg: FinTSConfig) { + const version = cfg.getMaxSupportedTransactionVersion('HKCCS') ?? 1; + return [ + { + header: { segId: 'HKVPP', segNr: 0, version: 1 }, + sepaDescriptor: 'urn:iso:std:iso:20022:tech:xsd:pain.002.001.10', + }, + { + header: { segId: 'HKCCS', segNr: 0, version }, + account: { iban: this.senderIban, bic: this.senderBic }, + sepaDescriptor: 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.09', + sepaPainMessage: this.painXml, + }, + ]; + } + + handleResponse(_r: unknown, cr: { bankAnswers?: { code: number; text: string }[] }) { + for (const a of cr.bankAnswers ?? []) { + this.bankResponses.push({ code: a.code, text: a.text }); + } + } +} +``` + +For SEPA Instant, submit `HKIPZ` in place of `HKCCS` (no `HKVPP` prefix is +required); settlement is asynchronous, so poll `HKIPS` until a terminal +state. See the inline documentation in `src/segments/HKIPZ.ts` for the full +set of bank response codes. + +## Sparkasse Berlin quirks + +Validated against real transfers on a Sparkasse Berlin account. The notes +below were observed on Sparkasse Berlin and may differ at other institutions. + +### pain.001 version +Use `urn:iso:std:iso:20022:tech:xsd:pain.001.001.09` (version 9). Older +schemas are silently rejected. + +### pain.002 descriptor in HKVPP +Declare `urn:iso:std:iso:20022:tech:xsd:pain.002.001.10` as the status +report descriptor. HKVPP must immediately precede HKCCS in the same +dialog — without it the bank rejects the whole submission before +processing the pain.001 payload. + +### `ReqdExctnDt` sentinel +Sparkasse rejects real execution dates with code **9150** ("Ausführungsdatum +ungültig"). Use the sentinel `1999-01-01` to mean "execute immediately". +Example: + +```xml +
1999-01-01
+``` + +### TAN method +pushTAN (method id **923**) is the only method currently accepted for +credit transfers. Poll for approval at ~3 s intervals; the bank's +internal timeout is around 120 s. + +### Successful response signature +A completed transfer returns two HIRMS messages with code **10**: + +```json +[ + { "code": 10, "text": "Nachricht entgegengenommen." }, + { "code": 10, "text": "Der Auftrag wurde entgegengenommen." } +] +``` + +### Failure codes worth handling explicitly +| Code | Meaning | +|------|---------| +| **3040** | Auftrag nur teilweise ausgeführt (partial execution — treat as failure) | +| **3945** | Freigabe kann nicht erteilt werden (pushTAN approval denied) | +| **9150** | Ausführungsdatum ungültig (use `1999-01-01` sentinel) | +| **9xxx** | Hard errors — abort the dialog | diff --git a/src/segments/HKCCS.ts b/src/segments/HKCCS.ts new file mode 100644 index 0000000..0bc3578 --- /dev/null +++ b/src/segments/HKCCS.ts @@ -0,0 +1,44 @@ +import { AlphaNumeric } from '../dataElements/AlphaNumeric.js'; +import { Binary } from '../dataElements/Binary.js'; +import { + type InternationalAccount, + InternationalAccountGroup, +} from '../dataGroups/InternationalAccount.js'; +import type { Segment } from '../segment.js'; +import { SegmentDefinition } from '../segmentDefinition.js'; + +export type HKCCSSegment = Segment & { + account: InternationalAccount; + sepaDescriptor: string; + sepaPainMessage: string; +}; + +/** + * Submit SEPA credit transfer (pain.001). + * + * Initiates a single SEPA wire transfer. The recipient account, amount + * and reference are carried inside the pain.001 XML payload — the FinTS + * segment itself only carries the sender account, the SEPA format + * descriptor and the XML message. + * + * Sparkasse Berlin quirks (validated against a live transfer): + * - Must be preceded by HKVPP declaring the pain.002 response descriptor. + * - pain.001 `ReqdExctnDt` must be the sentinel `1999-01-01` (real dates + * are rejected with code 9150). + * - TAN method 923 (pushTAN) is required for all transfers. + * + * See docs/HKCCS.md for the full protocol notes. + */ +export class HKCCS extends SegmentDefinition { + static Id = 'HKCCS'; + static Version = 1; + constructor() { + super(HKCCS.Id); + } + version = HKCCS.Version; + elements = [ + new InternationalAccountGroup('account', 1, 1), + new AlphaNumeric('sepaDescriptor', 1, 1, 256), + new Binary('sepaPainMessage', 1, 1, 99999), + ]; +} diff --git a/src/segments/HKIPZ.ts b/src/segments/HKIPZ.ts new file mode 100644 index 0000000..b81ec10 --- /dev/null +++ b/src/segments/HKIPZ.ts @@ -0,0 +1,64 @@ +import { AlphaNumeric } from '../dataElements/AlphaNumeric.js'; +import { Binary } from '../dataElements/Binary.js'; +import { YesNo } from '../dataElements/YesNo.js'; +import { + type InternationalAccount, + InternationalAccountGroup, +} from '../dataGroups/InternationalAccount.js'; +import type { Segment } from '../segment.js'; +import { SegmentDefinition } from '../segmentDefinition.js'; + +export type HKIPZSegment = Segment & { + account: InternationalAccount; + sepaDescriptor: string; + sepaPainMessage: string; + /** + * v2 only. When "J", the bank is permitted to fall back to a standard + * SEPA credit transfer if the receiver's bank is not reachable via SCT + * Inst or the amount exceeds the instant limit. Response code 3270 + * signals that such a conversion occurred. Some banks require a + * separate agreement before this flag takes effect. + */ + allowConversionToSepa?: boolean; +}; + +/** + * Submit SEPA instant credit transfer (SCT Inst / "Echtzeitüberweisung", + * pain.001). + * + * Parallel of HKCCS: initiates a single SEPA transfer intended for + * instant settlement (~10s). The recipient account, amount, reference + * and sender live inside the pain.001 XML payload; this segment carries + * the sender account, the SEPA format descriptor and the XML message. + * + * Response segment: HIIPZ (carries Auftragsidentifikation for subsequent + * HKIPS status polls) plus HIRMS bank answers. + * + * Important bank response codes: + * - 0020: Auftrag ausgeführt / Geld für den Empfänger verfügbar (success) + * - 3045: SEPA-Instant Payment Statusabfrage HKIPS veranlassen + * (settlement is async — poll HKIPS until terminal state) + * - 3270: Auftrag wird als Standard-SEPA-Überweisung bearbeitet + * (bank auto-fell-back to normal SEPA — only with v2 + + * `allowConversionToSepa = true`) + * - 9210: Betrag zu groß / Empfänger-IBAN existiert nicht / etc. + * - 9230: Unzureichendes Guthaben + * + * See C.10.2.9 "SEPA-Instant Payment Einzelzahlung" in the FinTS 3.0 + * Geschäftsvorfälle specification (2022-04-15) for the full protocol. + */ +export class HKIPZ extends SegmentDefinition { + static Id = 'HKIPZ'; + static Version = 2; + constructor() { + super(HKIPZ.Id); + } + version = HKIPZ.Version; + elements = [ + new InternationalAccountGroup('account', 1, 1), + new AlphaNumeric('sepaDescriptor', 1, 1, 256), + new Binary('sepaPainMessage', 1, 1, 99999), + // v2 only: "Umwandlung nach SEPA-Überweisung zulässig" + new YesNo('allowConversionToSepa', 0, 1, 2), + ]; +} diff --git a/src/segments/HKVPP.ts b/src/segments/HKVPP.ts new file mode 100644 index 0000000..b795ed1 --- /dev/null +++ b/src/segments/HKVPP.ts @@ -0,0 +1,25 @@ +import { AlphaNumeric } from '../dataElements/AlphaNumeric.js'; +import type { Segment } from '../segment.js'; +import { SegmentDefinition } from '../segmentDefinition.js'; + +export type HKVPPSegment = Segment & { + sepaDescriptor: string; +}; + +/** + * SEPA pain.002 descriptor prefix for HKCCS credit transfers. + * + * Declares the pain.002 schema version that the client supports for + * status reports. Sparkasse Berlin requires this segment immediately + * before every HKCCS submission — without it, transfers are rejected + * before the bank processes the pain.001 payload. + */ +export class HKVPP extends SegmentDefinition { + static Id = 'HKVPP'; + static Version = 1; + constructor() { + super(HKVPP.Id); + } + version = HKVPP.Version; + elements = [new AlphaNumeric('sepaDescriptor', 1, 1, 256)]; +} diff --git a/src/segments/registry.ts b/src/segments/registry.ts index 9ba2dcf..6a3ec24 100644 --- a/src/segments/registry.ts +++ b/src/segments/registry.ts @@ -24,14 +24,17 @@ import { HIUPA } from './HIUPA.js'; import { HIUPD } from './HIUPD.js'; import { HIWPD } from './HIWPD.js'; import { HKCAZ } from './HKCAZ.js'; +import { HKCCS } from './HKCCS.js'; import { HKEND } from './HKEND.js'; import { HKIDN } from './HKIDN.js'; +import { HKIPZ } from './HKIPZ.js'; import { HKKAZ } from './HKKAZ.js'; import { HKSAL } from './HKSAL.js'; import { HKSPA } from './HKSPA.js'; import { HKSYN } from './HKSYN.js'; import { HKTAB } from './HKTAB.js'; import { HKTAN } from './HKTAN.js'; +import { HKVPP } from './HKVPP.js'; import { HKVVB } from './HKVVB.js'; import { HKWPD } from './HKWPD.js'; import { HNHBK } from './HNHBK.js'; @@ -85,6 +88,9 @@ export function registerSegments() { registerSegmentDefinition(new HKSPA()); registerSegmentDefinition(new HISPA()); registerSegmentDefinition(new HISPAS()); + registerSegmentDefinition(new HKVPP()); + registerSegmentDefinition(new HKCCS()); + registerSegmentDefinition(new HKIPZ()); registerSegmentDefinition(new UNKNOW()); registerSegmentDefinition(new PARTED()); }