Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions docs/HKCCS.md
Original file line number Diff line number Diff line change
@@ -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
<ReqdExctnDt><Dt>1999-01-01</Dt></ReqdExctnDt>
```

### 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 |
44 changes: 44 additions & 0 deletions src/segments/HKCCS.ts
Original file line number Diff line number Diff line change
@@ -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),
];
}
64 changes: 64 additions & 0 deletions src/segments/HKIPZ.ts
Original file line number Diff line number Diff line change
@@ -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),
];
}
25 changes: 25 additions & 0 deletions src/segments/HKVPP.ts
Original file line number Diff line number Diff line change
@@ -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)];
}
6 changes: 6 additions & 0 deletions src/segments/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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());
}
Expand Down