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
96 changes: 96 additions & 0 deletions packages/fints/src/__tests__/test-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,100 @@ describe("Client", () => {
expect(resumedDialog.dialogId).toBe("9999");
expect(resumedDialog.msgNo).toBe(3);
});

test("capabilities returns bank features derived from the sync response", async () => {
// First call is the sync request; second call is the end request.
const syncResponse = {
success: true,
returnValues: () => new Map(),
dialogId: "sync-dialog",
systemId: "sys-1",
segmentMaxVersion: (cls: any) => {
const versionMap: Record<string, number> = {
HISALS: 5,
HIKAZS: 6,
HICDBS: 1,
HIDSES: 0,
HICCSS: 3,
HIWPDS: 6,
HITANS: 6,
};
return versionMap[cls.name] ?? 0;
},
supportedTanMethods: [] as any[],
painFormats: [] as string[],
findSegment: (cls: any) => {
if (cls.name === "HIKAZS") return { minSignatures: 1 };
if (cls.name === "HISALS") return { minSignatures: 0 };
return undefined;
},
findSegments: () => [] as any[],
};
const endResponse = {
success: true,
returnValues: () => new Map(),
dialogId: "0",
};

let callCount = 0;
const connection = {
send: jest.fn().mockImplementation(() => {
callCount++;
return Promise.resolve(callCount === 1 ? syncResponse : endResponse);
}),
};

const client = new TestClient(baseConfig, connection as any);
const caps = await client.capabilities();

expect(caps.supportsAccounts).toBe(true);
expect(caps.supportsBalance).toBe(true); // HISALS version 5
expect(caps.supportsTransactions).toBe(true); // HIKAZS version 6
expect(caps.supportsHoldings).toBe(true); // HIWPDS version 6
expect(caps.supportsStandingOrders).toBe(true); // HICDBS version 1
expect(caps.supportsCreditTransfer).toBe(true); // HICCSS version 3
expect(caps.supportsDirectDebit).toBe(false); // HIDSES version 0
expect(caps.requiresTanForTransactions).toBe(true); // minSignatures 1
expect(caps.requiresTanForBalance).toBe(false); // minSignatures 0
});

test("capabilities returns all false when bank advertises no optional features", async () => {
const syncResponse = {
success: true,
returnValues: () => new Map(),
dialogId: "sync-dialog",
systemId: "sys-1",
segmentMaxVersion: (_cls: any) => 0,
supportedTanMethods: [] as any[],
painFormats: [] as string[],
findSegment: (_cls: any): any => undefined,
findSegments: () => [] as any[],
};
const endResponse = {
success: true,
returnValues: () => new Map(),
dialogId: "0",
};

let callCount = 0;
const connection = {
send: jest.fn().mockImplementation(() => {
callCount++;
return Promise.resolve(callCount === 1 ? syncResponse : endResponse);
}),
};

const client = new TestClient(baseConfig, connection as any);
const caps = await client.capabilities();

expect(caps.supportsAccounts).toBe(true); // always true
expect(caps.supportsBalance).toBe(false);
expect(caps.supportsTransactions).toBe(false);
expect(caps.supportsHoldings).toBe(false);
expect(caps.supportsStandingOrders).toBe(false);
expect(caps.supportsCreditTransfer).toBe(false);
expect(caps.supportsDirectDebit).toBe(false);
expect(caps.requiresTanForTransactions).toBe(false);
expect(caps.requiresTanForBalance).toBe(false);
});
});
64 changes: 64 additions & 0 deletions packages/fints/src/__tests__/test-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,68 @@ describe("Dialog", () => {
expect(dialog.dialogId).toBe("4711");
}
});

test("capabilities getter reflects fields set during sync", () => {
const dialog = new Dialog(baseConfig, {} as any);

// Simulate the state after a sync response has been processed.
dialog.supportsBalance = true;
dialog.supportsTransactions = true;
dialog.hiwpdsVersion = 6;
dialog.supportsStandingOrders = true;
dialog.supportsCreditTransfer = true;
dialog.supportsDirectDebit = false;
dialog.hikazsMinSignatures = 1;
dialog.hisalsMinSignatures = 0;

const caps = dialog.capabilities;

expect(caps.supportsAccounts).toBe(true);
expect(caps.supportsBalance).toBe(true);
expect(caps.supportsTransactions).toBe(true);
expect(caps.supportsHoldings).toBe(true);
expect(caps.supportsStandingOrders).toBe(true);
expect(caps.supportsCreditTransfer).toBe(true);
expect(caps.supportsDirectDebit).toBe(false);
expect(caps.requiresTanForTransactions).toBe(true);
expect(caps.requiresTanForBalance).toBe(false);
});

test("capabilities getter returns false for unsupported features", () => {
const dialog = new Dialog(baseConfig, {} as any);

// Simulate a bank that advertises no optional features.
dialog.supportsBalance = false;
dialog.supportsTransactions = false;
dialog.hiwpdsVersion = 0;
dialog.supportsStandingOrders = false;
dialog.supportsCreditTransfer = false;
dialog.supportsDirectDebit = false;
dialog.hikazsMinSignatures = 0;
dialog.hisalsMinSignatures = 0;

const caps = dialog.capabilities;

expect(caps.supportsAccounts).toBe(true); // always true
expect(caps.supportsBalance).toBe(false);
expect(caps.supportsTransactions).toBe(false);
expect(caps.supportsHoldings).toBe(false);
expect(caps.supportsStandingOrders).toBe(false);
expect(caps.supportsCreditTransfer).toBe(false);
expect(caps.supportsDirectDebit).toBe(false);
expect(caps.requiresTanForTransactions).toBe(false);
expect(caps.requiresTanForBalance).toBe(false);
});

test("capabilities getter returns false before sync() is called", () => {
const dialog = new Dialog(baseConfig, {} as any);
// No sync() has run - all support flags should be false
const caps = dialog.capabilities;
expect(caps.supportsBalance).toBe(false);
expect(caps.supportsTransactions).toBe(false);
expect(caps.supportsHoldings).toBe(false);
expect(caps.supportsStandingOrders).toBe(false);
expect(caps.supportsCreditTransfer).toBe(false);
expect(caps.supportsDirectDebit).toBe(false);
});
});
16 changes: 16 additions & 0 deletions packages/fints/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
DirectDebitSubmission,
CreditTransferRequest,
CreditTransferSubmission,
BankCapabilities,
} from "./types";
import { read } from "mt940-js";
import { parse86Structured } from "./mt940-86-structured";
Expand All @@ -57,6 +58,21 @@ export abstract class Client {
*/
protected abstract createRequest(dialog: Dialog, segments: Segment<any>[], tan?: string): Request;

/**
* Retrieve the capabilities of the bank by performing a synchronisation request.
*
* The capabilities are derived from the parameter segments the bank advertises during
* the initial sync dialog (e.g. HIKAZS, HISALS, HIWPDS, HICCSS, HIDSES, …).
* No additional authentication beyond the configured credentials is required.
*
* @return An object describing what operations this bank supports.
*/
public async capabilities(): Promise<BankCapabilities> {
const dialog = this.createDialog();
await dialog.sync();
return dialog.capabilities;
}
Comment on lines +70 to +74
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description focuses on adding BankCapabilities, but the diff also removes IMPLEMENTATION_SUMMARY.md entirely. If that removal is intentional cleanup, please mention it in the PR description or move it to a separate PR to keep this change focused.

Copilot uses AI. Check for mistakes.

/**
* Fetch a list of all SEPA accounts accessible by the user.
*
Expand Down
76 changes: 72 additions & 4 deletions packages/fints/src/dialog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Connection } from "./types";
import { Connection, BankCapabilities } from "./types";
import {
HKIDN,
HKVVB,
Expand Down Expand Up @@ -103,6 +103,41 @@ export class Dialog extends DialogConfig {
* Stores the maximum supported version parsed during synchronization.
*/
public hiwpdsVersion = 0;
/**
* Whether the bank supports querying account balances (HKSAL).
* Set to `true` during synchronization if the bank returns a HISALS parameter segment.
*/
public supportsBalance = false;
/**
* Whether the bank supports fetching bank statements / transaction history (HKKAZ).
* Set to `true` during synchronization if the bank returns a HIKAZS parameter segment.
*/
public supportsTransactions = false;
/**
* Whether the bank supports fetching standing orders (HKCDB).
* Set to `true` during synchronization if the bank returns a HICDBS parameter segment.
*/
public supportsStandingOrders = false;
/**
* Whether the bank supports SEPA credit transfers (HKCCS).
* Set to `true` during synchronization if the bank returns a HICCSS parameter segment.
*/
public supportsCreditTransfer = false;
/**
* Whether the bank supports SEPA direct debits (HKDSE).
* Set to `true` during synchronization if the bank returns a HIDSES parameter segment.
*/
public supportsDirectDebit = false;
/**
* Minimum number of signatures required to fetch bank statements (from HIKAZS).
* A value greater than `0` means a TAN is required.
*/
public hikazsMinSignatures = 0;
/**
* Minimum number of signatures required to query account balances (from HISALS).
* A value greater than `0` means a TAN is required.
*/
public hisalsMinSignatures = 0;
/**
* A list of supported SEPA pain-formats as configured by the server.
*/
Expand Down Expand Up @@ -148,17 +183,29 @@ export class Dialog extends DialogConfig {
const response = await this.send(new Request({ blz, name, pin, systemId, dialogId, msgNo, segments }));
this.systemId = escapeFinTS(response.systemId);
this.dialogId = response.dialogId;
this.hisalsVersion = response.segmentMaxVersion(HISALS);
this.hikazsVersion = response.segmentMaxVersion(HIKAZS);
this.hicdbVersion = response.segmentMaxVersion(HICDBS);
const hisalsVer = response.segmentMaxVersion(HISALS);
this.supportsBalance = hisalsVer > 0;
if (hisalsVer > 0) this.hisalsVersion = hisalsVer;
const hikazsVer = response.segmentMaxVersion(HIKAZS);
this.supportsTransactions = hikazsVer > 0;
if (hikazsVer > 0) this.hikazsVersion = hikazsVer;
const hicdbVer = response.segmentMaxVersion(HICDBS);
this.supportsStandingOrders = hicdbVer > 0;
if (hicdbVer > 0) this.hicdbVersion = hicdbVer;
const hkdseVersion = response.segmentMaxVersion(HIDSES);
this.hkdseVersion = hkdseVersion > 0 ? hkdseVersion : 1;
this.supportsDirectDebit = hkdseVersion > 0;
const hkccsVersion = response.segmentMaxVersion(HICCSS);
this.hkccsVersion = hkccsVersion > 0 ? hkccsVersion : 1;
this.supportsCreditTransfer = hkccsVersion > 0;
this.hiwpdsVersion = response.segmentMaxVersion(HIWPDS);
this.hktanVersion = response.segmentMaxVersion(HITANS);
this.tanMethods = response.supportedTanMethods;
this.painFormats = response.painFormats;
const hikazs = response.findSegment(HIKAZS);
this.hikazsMinSignatures = hikazs?.minSignatures ?? 0;
const hisals = response.findSegment(HISALS);
this.hisalsMinSignatures = hisals?.minSignatures ?? 0;
Comment on lines +205 to +208

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive TAN requirements from negotiated segment version

sync() determines supported HIKAZS/HISALS versions via segmentMaxVersion(...), but requiresTanForTransactions/requiresTanForBalance are populated from response.findSegment(...), which returns the first segment instance, not necessarily the version that will actually be used by requests. When a bank advertises multiple versions of these parameter segments and minSignatures differs between versions, dialog.capabilities can report incorrect TAN requirements; read minSignatures from the selected max version (or aggregate safely across all advertised versions).

Useful? React with 👍 / 👎.

const hiupd = response.findSegments(HIUPD);
this.hiupd = hiupd;
await this.end();
Expand Down Expand Up @@ -326,4 +373,25 @@ export class Dialog extends DialogConfig {
this.decoupledTanManager = undefined;
}
}

/**
* Returns the capabilities of the bank based on the parameter segments
* received during the last synchronisation.
*
* Call this only after `sync()` has been invoked so that all version
* fields have been populated from the server response.
*/
public get capabilities(): BankCapabilities {
return {
supportsAccounts: true,
supportsBalance: this.supportsBalance,
supportsTransactions: this.supportsTransactions,
supportsHoldings: this.hiwpdsVersion > 0,
supportsStandingOrders: this.supportsStandingOrders,
supportsCreditTransfer: this.supportsCreditTransfer,
supportsDirectDebit: this.supportsDirectDebit,
requiresTanForTransactions: this.hikazsMinSignatures > 0,
requiresTanForBalance: this.hisalsMinSignatures > 0,
};
}
}
54 changes: 54 additions & 0 deletions packages/fints/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,60 @@ export interface Holding {
acquisitionPrice?: number;
}

/**
* Describes the capabilities of a bank as determined during the initial synchronisation dialog.
*
* Each flag reflects whether the bank advertises support for the corresponding FinTS business
* transaction via its parameter segments (e.g. HIKAZS, HISALS, HIWPDS, …).
*/
export interface BankCapabilities {
/**
* Whether the bank supports retrieving the list of SEPA accounts (HKSPA).
* This is always `true` for any conforming FinTS server.
*/
supportsAccounts: boolean;
/**
* Whether the bank supports querying account balances (HKSAL).
* Derived from the presence of a HISALS parameter segment in the sync response.
*/
supportsBalance: boolean;
/**
* Whether the bank supports fetching bank statements / transaction history (HKKAZ).
* Derived from the presence of a HIKAZS parameter segment in the sync response.
*/
supportsTransactions: boolean;
/**
* Whether the bank supports fetching securities and holdings for a depot account (HKWPD).
* Derived from the presence of a HIWPDS parameter segment in the sync response.
*/
supportsHoldings: boolean;
/**
* Whether the bank supports fetching standing orders (HKCDB).
* Derived from the presence of a HICDBS parameter segment in the sync response.
*/
supportsStandingOrders: boolean;
/**
* Whether the bank supports initiating SEPA credit transfers (HKCCS).
* Derived from the presence of a HICCSS parameter segment in the sync response.
*/
supportsCreditTransfer: boolean;
/**
* Whether the bank supports submitting SEPA direct debits (HKDSE).
* Derived from the presence of a HIDSES parameter segment in the sync response.
*/
supportsDirectDebit: boolean;
/**
* Whether a TAN is required to fetch bank statements.
* Derived from the `minSignatures` field of the HIKAZS parameter segment (`minSignatures > 0`).
*/
requiresTanForTransactions: boolean;
/**
* Whether a TAN is required to query account balances.
* Derived from the `minSignatures` field of the HISALS parameter segment (`minSignatures > 0`).
*/
requiresTanForBalance: boolean;
}

/**
* A connection used in the client to contact the server.
*/
Expand Down
Loading