Skip to content

Commit 2578347

Browse files
authored
Merge pull request #36 from larsdecker/copilot/add-bank-capabilities-schema
feat: Add BankCapabilities to expose bank feature flags
2 parents 5c6941c + 318cfba commit 2578347

5 files changed

Lines changed: 302 additions & 4 deletions

File tree

packages/fints/src/__tests__/test-client.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,100 @@ describe("Client", () => {
6666
expect(resumedDialog.dialogId).toBe("9999");
6767
expect(resumedDialog.msgNo).toBe(3);
6868
});
69+
70+
test("capabilities returns bank features derived from the sync response", async () => {
71+
// First call is the sync request; second call is the end request.
72+
const syncResponse = {
73+
success: true,
74+
returnValues: () => new Map(),
75+
dialogId: "sync-dialog",
76+
systemId: "sys-1",
77+
segmentMaxVersion: (cls: any) => {
78+
const versionMap: Record<string, number> = {
79+
HISALS: 5,
80+
HIKAZS: 6,
81+
HICDBS: 1,
82+
HIDSES: 0,
83+
HICCSS: 3,
84+
HIWPDS: 6,
85+
HITANS: 6,
86+
};
87+
return versionMap[cls.name] ?? 0;
88+
},
89+
supportedTanMethods: [] as any[],
90+
painFormats: [] as string[],
91+
findSegment: (cls: any) => {
92+
if (cls.name === "HIKAZS") return { minSignatures: 1 };
93+
if (cls.name === "HISALS") return { minSignatures: 0 };
94+
return undefined;
95+
},
96+
findSegments: () => [] as any[],
97+
};
98+
const endResponse = {
99+
success: true,
100+
returnValues: () => new Map(),
101+
dialogId: "0",
102+
};
103+
104+
let callCount = 0;
105+
const connection = {
106+
send: jest.fn().mockImplementation(() => {
107+
callCount++;
108+
return Promise.resolve(callCount === 1 ? syncResponse : endResponse);
109+
}),
110+
};
111+
112+
const client = new TestClient(baseConfig, connection as any);
113+
const caps = await client.capabilities();
114+
115+
expect(caps.supportsAccounts).toBe(true);
116+
expect(caps.supportsBalance).toBe(true); // HISALS version 5
117+
expect(caps.supportsTransactions).toBe(true); // HIKAZS version 6
118+
expect(caps.supportsHoldings).toBe(true); // HIWPDS version 6
119+
expect(caps.supportsStandingOrders).toBe(true); // HICDBS version 1
120+
expect(caps.supportsCreditTransfer).toBe(true); // HICCSS version 3
121+
expect(caps.supportsDirectDebit).toBe(false); // HIDSES version 0
122+
expect(caps.requiresTanForTransactions).toBe(true); // minSignatures 1
123+
expect(caps.requiresTanForBalance).toBe(false); // minSignatures 0
124+
});
125+
126+
test("capabilities returns all false when bank advertises no optional features", async () => {
127+
const syncResponse = {
128+
success: true,
129+
returnValues: () => new Map(),
130+
dialogId: "sync-dialog",
131+
systemId: "sys-1",
132+
segmentMaxVersion: (_cls: any) => 0,
133+
supportedTanMethods: [] as any[],
134+
painFormats: [] as string[],
135+
findSegment: (_cls: any): any => undefined,
136+
findSegments: () => [] as any[],
137+
};
138+
const endResponse = {
139+
success: true,
140+
returnValues: () => new Map(),
141+
dialogId: "0",
142+
};
143+
144+
let callCount = 0;
145+
const connection = {
146+
send: jest.fn().mockImplementation(() => {
147+
callCount++;
148+
return Promise.resolve(callCount === 1 ? syncResponse : endResponse);
149+
}),
150+
};
151+
152+
const client = new TestClient(baseConfig, connection as any);
153+
const caps = await client.capabilities();
154+
155+
expect(caps.supportsAccounts).toBe(true); // always true
156+
expect(caps.supportsBalance).toBe(false);
157+
expect(caps.supportsTransactions).toBe(false);
158+
expect(caps.supportsHoldings).toBe(false);
159+
expect(caps.supportsStandingOrders).toBe(false);
160+
expect(caps.supportsCreditTransfer).toBe(false);
161+
expect(caps.supportsDirectDebit).toBe(false);
162+
expect(caps.requiresTanForTransactions).toBe(false);
163+
expect(caps.requiresTanForBalance).toBe(false);
164+
});
69165
});

packages/fints/src/__tests__/test-dialog.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,68 @@ describe("Dialog", () => {
5252
expect(dialog.dialogId).toBe("4711");
5353
}
5454
});
55+
56+
test("capabilities getter reflects fields set during sync", () => {
57+
const dialog = new Dialog(baseConfig, {} as any);
58+
59+
// Simulate the state after a sync response has been processed.
60+
dialog.supportsBalance = true;
61+
dialog.supportsTransactions = true;
62+
dialog.hiwpdsVersion = 6;
63+
dialog.supportsStandingOrders = true;
64+
dialog.supportsCreditTransfer = true;
65+
dialog.supportsDirectDebit = false;
66+
dialog.hikazsMinSignatures = 1;
67+
dialog.hisalsMinSignatures = 0;
68+
69+
const caps = dialog.capabilities;
70+
71+
expect(caps.supportsAccounts).toBe(true);
72+
expect(caps.supportsBalance).toBe(true);
73+
expect(caps.supportsTransactions).toBe(true);
74+
expect(caps.supportsHoldings).toBe(true);
75+
expect(caps.supportsStandingOrders).toBe(true);
76+
expect(caps.supportsCreditTransfer).toBe(true);
77+
expect(caps.supportsDirectDebit).toBe(false);
78+
expect(caps.requiresTanForTransactions).toBe(true);
79+
expect(caps.requiresTanForBalance).toBe(false);
80+
});
81+
82+
test("capabilities getter returns false for unsupported features", () => {
83+
const dialog = new Dialog(baseConfig, {} as any);
84+
85+
// Simulate a bank that advertises no optional features.
86+
dialog.supportsBalance = false;
87+
dialog.supportsTransactions = false;
88+
dialog.hiwpdsVersion = 0;
89+
dialog.supportsStandingOrders = false;
90+
dialog.supportsCreditTransfer = false;
91+
dialog.supportsDirectDebit = false;
92+
dialog.hikazsMinSignatures = 0;
93+
dialog.hisalsMinSignatures = 0;
94+
95+
const caps = dialog.capabilities;
96+
97+
expect(caps.supportsAccounts).toBe(true); // always true
98+
expect(caps.supportsBalance).toBe(false);
99+
expect(caps.supportsTransactions).toBe(false);
100+
expect(caps.supportsHoldings).toBe(false);
101+
expect(caps.supportsStandingOrders).toBe(false);
102+
expect(caps.supportsCreditTransfer).toBe(false);
103+
expect(caps.supportsDirectDebit).toBe(false);
104+
expect(caps.requiresTanForTransactions).toBe(false);
105+
expect(caps.requiresTanForBalance).toBe(false);
106+
});
107+
108+
test("capabilities getter returns false before sync() is called", () => {
109+
const dialog = new Dialog(baseConfig, {} as any);
110+
// No sync() has run - all support flags should be false
111+
const caps = dialog.capabilities;
112+
expect(caps.supportsBalance).toBe(false);
113+
expect(caps.supportsTransactions).toBe(false);
114+
expect(caps.supportsHoldings).toBe(false);
115+
expect(caps.supportsStandingOrders).toBe(false);
116+
expect(caps.supportsCreditTransfer).toBe(false);
117+
expect(caps.supportsDirectDebit).toBe(false);
118+
});
55119
});

packages/fints/src/client.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
DirectDebitSubmission,
3232
CreditTransferRequest,
3333
CreditTransferSubmission,
34+
BankCapabilities,
3435
} from "./types";
3536
import { read } from "mt940-js";
3637
import { parse86Structured } from "./mt940-86-structured";
@@ -57,6 +58,21 @@ export abstract class Client {
5758
*/
5859
protected abstract createRequest(dialog: Dialog, segments: Segment<any>[], tan?: string): Request;
5960

61+
/**
62+
* Retrieve the capabilities of the bank by performing a synchronisation request.
63+
*
64+
* The capabilities are derived from the parameter segments the bank advertises during
65+
* the initial sync dialog (e.g. HIKAZS, HISALS, HIWPDS, HICCSS, HIDSES, …).
66+
* No additional authentication beyond the configured credentials is required.
67+
*
68+
* @return An object describing what operations this bank supports.
69+
*/
70+
public async capabilities(): Promise<BankCapabilities> {
71+
const dialog = this.createDialog();
72+
await dialog.sync();
73+
return dialog.capabilities;
74+
}
75+
6076
/**
6177
* Fetch a list of all SEPA accounts accessible by the user.
6278
*

packages/fints/src/dialog.ts

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Connection } from "./types";
1+
import { Connection, BankCapabilities } from "./types";
22
import {
33
HKIDN,
44
HKVVB,
@@ -103,6 +103,41 @@ export class Dialog extends DialogConfig {
103103
* Stores the maximum supported version parsed during synchronization.
104104
*/
105105
public hiwpdsVersion = 0;
106+
/**
107+
* Whether the bank supports querying account balances (HKSAL).
108+
* Set to `true` during synchronization if the bank returns a HISALS parameter segment.
109+
*/
110+
public supportsBalance = false;
111+
/**
112+
* Whether the bank supports fetching bank statements / transaction history (HKKAZ).
113+
* Set to `true` during synchronization if the bank returns a HIKAZS parameter segment.
114+
*/
115+
public supportsTransactions = false;
116+
/**
117+
* Whether the bank supports fetching standing orders (HKCDB).
118+
* Set to `true` during synchronization if the bank returns a HICDBS parameter segment.
119+
*/
120+
public supportsStandingOrders = false;
121+
/**
122+
* Whether the bank supports SEPA credit transfers (HKCCS).
123+
* Set to `true` during synchronization if the bank returns a HICCSS parameter segment.
124+
*/
125+
public supportsCreditTransfer = false;
126+
/**
127+
* Whether the bank supports SEPA direct debits (HKDSE).
128+
* Set to `true` during synchronization if the bank returns a HIDSES parameter segment.
129+
*/
130+
public supportsDirectDebit = false;
131+
/**
132+
* Minimum number of signatures required to fetch bank statements (from HIKAZS).
133+
* A value greater than `0` means a TAN is required.
134+
*/
135+
public hikazsMinSignatures = 0;
136+
/**
137+
* Minimum number of signatures required to query account balances (from HISALS).
138+
* A value greater than `0` means a TAN is required.
139+
*/
140+
public hisalsMinSignatures = 0;
106141
/**
107142
* A list of supported SEPA pain-formats as configured by the server.
108143
*/
@@ -148,17 +183,29 @@ export class Dialog extends DialogConfig {
148183
const response = await this.send(new Request({ blz, name, pin, systemId, dialogId, msgNo, segments }));
149184
this.systemId = escapeFinTS(response.systemId);
150185
this.dialogId = response.dialogId;
151-
this.hisalsVersion = response.segmentMaxVersion(HISALS);
152-
this.hikazsVersion = response.segmentMaxVersion(HIKAZS);
153-
this.hicdbVersion = response.segmentMaxVersion(HICDBS);
186+
const hisalsVer = response.segmentMaxVersion(HISALS);
187+
this.supportsBalance = hisalsVer > 0;
188+
if (hisalsVer > 0) this.hisalsVersion = hisalsVer;
189+
const hikazsVer = response.segmentMaxVersion(HIKAZS);
190+
this.supportsTransactions = hikazsVer > 0;
191+
if (hikazsVer > 0) this.hikazsVersion = hikazsVer;
192+
const hicdbVer = response.segmentMaxVersion(HICDBS);
193+
this.supportsStandingOrders = hicdbVer > 0;
194+
if (hicdbVer > 0) this.hicdbVersion = hicdbVer;
154195
const hkdseVersion = response.segmentMaxVersion(HIDSES);
155196
this.hkdseVersion = hkdseVersion > 0 ? hkdseVersion : 1;
197+
this.supportsDirectDebit = hkdseVersion > 0;
156198
const hkccsVersion = response.segmentMaxVersion(HICCSS);
157199
this.hkccsVersion = hkccsVersion > 0 ? hkccsVersion : 1;
200+
this.supportsCreditTransfer = hkccsVersion > 0;
158201
this.hiwpdsVersion = response.segmentMaxVersion(HIWPDS);
159202
this.hktanVersion = response.segmentMaxVersion(HITANS);
160203
this.tanMethods = response.supportedTanMethods;
161204
this.painFormats = response.painFormats;
205+
const hikazs = response.findSegment(HIKAZS);
206+
this.hikazsMinSignatures = hikazs?.minSignatures ?? 0;
207+
const hisals = response.findSegment(HISALS);
208+
this.hisalsMinSignatures = hisals?.minSignatures ?? 0;
162209
const hiupd = response.findSegments(HIUPD);
163210
this.hiupd = hiupd;
164211
await this.end();
@@ -326,4 +373,25 @@ export class Dialog extends DialogConfig {
326373
this.decoupledTanManager = undefined;
327374
}
328375
}
376+
377+
/**
378+
* Returns the capabilities of the bank based on the parameter segments
379+
* received during the last synchronisation.
380+
*
381+
* Call this only after `sync()` has been invoked so that all version
382+
* fields have been populated from the server response.
383+
*/
384+
public get capabilities(): BankCapabilities {
385+
return {
386+
supportsAccounts: true,
387+
supportsBalance: this.supportsBalance,
388+
supportsTransactions: this.supportsTransactions,
389+
supportsHoldings: this.hiwpdsVersion > 0,
390+
supportsStandingOrders: this.supportsStandingOrders,
391+
supportsCreditTransfer: this.supportsCreditTransfer,
392+
supportsDirectDebit: this.supportsDirectDebit,
393+
requiresTanForTransactions: this.hikazsMinSignatures > 0,
394+
requiresTanForBalance: this.hisalsMinSignatures > 0,
395+
};
396+
}
329397
}

packages/fints/src/types.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,60 @@ export interface Holding {
394394
acquisitionPrice?: number;
395395
}
396396

397+
/**
398+
* Describes the capabilities of a bank as determined during the initial synchronisation dialog.
399+
*
400+
* Each flag reflects whether the bank advertises support for the corresponding FinTS business
401+
* transaction via its parameter segments (e.g. HIKAZS, HISALS, HIWPDS, …).
402+
*/
403+
export interface BankCapabilities {
404+
/**
405+
* Whether the bank supports retrieving the list of SEPA accounts (HKSPA).
406+
* This is always `true` for any conforming FinTS server.
407+
*/
408+
supportsAccounts: boolean;
409+
/**
410+
* Whether the bank supports querying account balances (HKSAL).
411+
* Derived from the presence of a HISALS parameter segment in the sync response.
412+
*/
413+
supportsBalance: boolean;
414+
/**
415+
* Whether the bank supports fetching bank statements / transaction history (HKKAZ).
416+
* Derived from the presence of a HIKAZS parameter segment in the sync response.
417+
*/
418+
supportsTransactions: boolean;
419+
/**
420+
* Whether the bank supports fetching securities and holdings for a depot account (HKWPD).
421+
* Derived from the presence of a HIWPDS parameter segment in the sync response.
422+
*/
423+
supportsHoldings: boolean;
424+
/**
425+
* Whether the bank supports fetching standing orders (HKCDB).
426+
* Derived from the presence of a HICDBS parameter segment in the sync response.
427+
*/
428+
supportsStandingOrders: boolean;
429+
/**
430+
* Whether the bank supports initiating SEPA credit transfers (HKCCS).
431+
* Derived from the presence of a HICCSS parameter segment in the sync response.
432+
*/
433+
supportsCreditTransfer: boolean;
434+
/**
435+
* Whether the bank supports submitting SEPA direct debits (HKDSE).
436+
* Derived from the presence of a HIDSES parameter segment in the sync response.
437+
*/
438+
supportsDirectDebit: boolean;
439+
/**
440+
* Whether a TAN is required to fetch bank statements.
441+
* Derived from the `minSignatures` field of the HIKAZS parameter segment (`minSignatures > 0`).
442+
*/
443+
requiresTanForTransactions: boolean;
444+
/**
445+
* Whether a TAN is required to query account balances.
446+
* Derived from the `minSignatures` field of the HISALS parameter segment (`minSignatures > 0`).
447+
*/
448+
requiresTanForBalance: boolean;
449+
}
450+
397451
/**
398452
* A connection used in the client to contact the server.
399453
*/

0 commit comments

Comments
 (0)