lib-fints 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +21 -17
  2. package/dist/accountDescriptor.js +65 -0
  3. package/dist/bankAccount.js +8 -0
  4. package/dist/client.js +41 -40
  5. package/dist/config.js +57 -10
  6. package/dist/interactions/balanceInteraction.js +10 -8
  7. package/dist/interactions/creditcardStatementInteraction.js +8 -7
  8. package/dist/interactions/electronicStatementInteraction.js +6 -5
  9. package/dist/interactions/portfolioInteraction.js +9 -7
  10. package/dist/interactions/sepaAccountInteraction.js +6 -5
  11. package/dist/interactions/statementInteractionCAMT.js +6 -5
  12. package/dist/interactions/statementInteractionMT940.js +7 -6
  13. package/dist/message.js +5 -1
  14. package/dist/segments/HKKAZ.js +1 -1
  15. package/dist/tests/accountDescriptor.test.js +99 -0
  16. package/dist/tests/accountReference.test.js +126 -0
  17. package/dist/tests/interactionAccountDescriptor.test.js +153 -0
  18. package/dist/tests/message.test.js +18 -0
  19. package/dist/types/accountDescriptor.d.ts +49 -0
  20. package/dist/types/accountDescriptor.d.ts.map +1 -0
  21. package/dist/types/bankAccount.d.ts +13 -0
  22. package/dist/types/bankAccount.d.ts.map +1 -1
  23. package/dist/types/client.d.ts +21 -20
  24. package/dist/types/client.d.ts.map +1 -1
  25. package/dist/types/config.d.ts +27 -6
  26. package/dist/types/config.d.ts.map +1 -1
  27. package/dist/types/index.d.ts +4 -4
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/interactions/balanceInteraction.d.ts +3 -2
  30. package/dist/types/interactions/balanceInteraction.d.ts.map +1 -1
  31. package/dist/types/interactions/creditcardStatementInteraction.d.ts +3 -2
  32. package/dist/types/interactions/creditcardStatementInteraction.d.ts.map +1 -1
  33. package/dist/types/interactions/electronicStatementInteraction.d.ts +3 -2
  34. package/dist/types/interactions/electronicStatementInteraction.d.ts.map +1 -1
  35. package/dist/types/interactions/portfolioInteraction.d.ts +3 -2
  36. package/dist/types/interactions/portfolioInteraction.d.ts.map +1 -1
  37. package/dist/types/interactions/sepaAccountInteraction.d.ts +3 -2
  38. package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -1
  39. package/dist/types/interactions/statementInteractionCAMT.d.ts +3 -2
  40. package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -1
  41. package/dist/types/interactions/statementInteractionMT940.d.ts +3 -2
  42. package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -1
  43. package/dist/types/message.d.ts.map +1 -1
  44. package/dist/types/mt940parser.d.ts +2 -2
  45. package/dist/types/mt940parser.d.ts.map +1 -1
  46. package/dist/types/segments/HKKAZ.d.ts +2 -2
  47. package/dist/types/segments/HKKAZ.d.ts.map +1 -1
  48. package/dist/types/tests/accountDescriptor.test.d.ts +2 -0
  49. package/dist/types/tests/accountDescriptor.test.d.ts.map +1 -0
  50. package/dist/types/tests/accountReference.test.d.ts +2 -0
  51. package/dist/types/tests/accountReference.test.d.ts.map +1 -0
  52. package/dist/types/tests/interactionAccountDescriptor.test.d.ts +2 -0
  53. package/dist/types/tests/interactionAccountDescriptor.test.d.ts.map +1 -0
  54. package/package.json +3 -3
@@ -1,19 +1,20 @@
1
+ import { internationalAccount } from '../accountDescriptor.js';
1
2
  import { CamtParser } from '../camtParser.js';
2
3
  import { HICAZ } from '../segments/HICAZ.js';
3
4
  import { HKCAZ } from '../segments/HKCAZ.js';
4
5
  import { CustomerOrderInteraction } from './customerInteraction.js';
5
6
  export class StatementInteractionCAMT extends CustomerOrderInteraction {
6
- accountNumber;
7
+ account;
7
8
  from;
8
9
  to;
9
- constructor(accountNumber, from, to) {
10
+ constructor(account, from, to) {
10
11
  super(HKCAZ.Id, HICAZ.Id);
11
- this.accountNumber = accountNumber;
12
+ this.account = account;
12
13
  this.from = from;
13
14
  this.to = to;
14
15
  }
15
16
  createSegments(init) {
16
- const bankAccount = init.getBankAccount(this.accountNumber);
17
+ const bankAccount = init.getBankAccount(this.account);
17
18
  const version = init.getMaxSupportedTransactionVersion(HKCAZ.Id);
18
19
  if (!version) {
19
20
  throw Error(`There is no supported version for business transaction '${HKCAZ.Id}'`);
@@ -25,7 +26,7 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction {
25
26
  }
26
27
  const hkcaz = {
27
28
  header: { segId: HKCAZ.Id, segNr: 0, version: version },
28
- account: bankAccount,
29
+ account: internationalAccount(init, bankAccount),
29
30
  acceptedCamtFormats: acceptedCamtFormats,
30
31
  allAccounts: false,
31
32
  from: this.from,
@@ -1,27 +1,28 @@
1
+ import { internationalAccount, nationalAccount } from '../accountDescriptor.js';
1
2
  import { Mt940Parser } from '../mt940parser.js';
2
3
  import { HIKAZ } from '../segments/HIKAZ.js';
3
4
  import { HKKAZ } from '../segments/HKKAZ.js';
4
5
  import { CustomerOrderInteraction } from './customerInteraction.js';
5
6
  export class StatementInteractionMT940 extends CustomerOrderInteraction {
6
- accountNumber;
7
+ account;
7
8
  from;
8
9
  to;
9
- constructor(accountNumber, from, to) {
10
+ constructor(account, from, to) {
10
11
  super(HKKAZ.Id, HIKAZ.Id);
11
- this.accountNumber = accountNumber;
12
+ this.account = account;
12
13
  this.from = from;
13
14
  this.to = to;
14
15
  }
15
16
  createSegments(init) {
16
- const bankAccount = init.getBankAccount(this.accountNumber);
17
- const account = { ...bankAccount, iban: undefined };
17
+ const bankAccount = init.getBankAccount(this.account);
18
18
  const version = init.getMaxSupportedTransactionVersion(HKKAZ.Id);
19
19
  if (!version) {
20
20
  throw Error(`There is no supported version for business transaction '${HKKAZ.Id}'`);
21
21
  }
22
+ const account = version <= 6 ? nationalAccount(bankAccount) : internationalAccount(init, bankAccount);
22
23
  const hkkaz = {
23
24
  header: { segId: HKKAZ.Id, segNr: 0, version: version },
24
- account,
25
+ account: account,
25
26
  allAccounts: false,
26
27
  from: this.from,
27
28
  to: this.to,
package/dist/message.js CHANGED
@@ -143,9 +143,13 @@ export class CustomerMessage extends Message {
143
143
  throw new Error('no signature segment found in message');
144
144
  }
145
145
  const now = new Date();
146
+ // The encryption header carries the same security profile as the
147
+ // signature: PIN version 2 for a two-step TAN method, 1 for one-step.
148
+ // Some banks (Consorsbank, BLZ 76030080) reject a two-step dialog whose
149
+ // HNVSK still says PIN:1 with "9010 Ungültiger Signaturaufbau".
146
150
  const hnvsk = {
147
151
  header: { segId: HNVSK.Id, segNr: 998, version: HNVSK.Version },
148
- secProfile: { secMethod: 'PIN', secVersion: 1 },
152
+ secProfile: { secMethod: 'PIN', secVersion: firstSignature.secProfile.secVersion },
149
153
  secFunc: 998,
150
154
  secRole: 1,
151
155
  secId: { partyType: 1, partyId: firstSignature.secId.partyId },
@@ -3,7 +3,7 @@ import { Dat } from '../dataElements/Dat.js';
3
3
  import { Numeric } from '../dataElements/Numeric.js';
4
4
  import { YesNo } from '../dataElements/YesNo.js';
5
5
  import { AccountGroup } from '../dataGroups/Account.js';
6
- import { InternationalAccountGroup } from '../dataGroups/InternationalAccount.js';
6
+ import { InternationalAccountGroup, } from '../dataGroups/InternationalAccount.js';
7
7
  import { SegmentDefinition } from '../segmentDefinition.js';
8
8
  /**
9
9
  * Request account transactions in a given period
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { internationalAccount, nationalAccount } from '../accountDescriptor.js';
3
+ import { AccountType } from '../bankAccount.js';
4
+ import { Language } from '../codes.js';
5
+ import { FinTSConfig } from '../config.js';
6
+ const account = {
7
+ accountNumber: '1234567890',
8
+ subAccountId: 'Girokonto',
9
+ bank: { country: 280, bankId: '10020030' },
10
+ iban: 'DE89370400440532013000',
11
+ bic: 'BANKDEFFXXX',
12
+ customerId: 'customer1',
13
+ accountType: AccountType.CheckingAccount,
14
+ currency: 'EUR',
15
+ holder1: 'Test User',
16
+ };
17
+ const depot = {
18
+ ...account,
19
+ accountNumber: '9876543210',
20
+ iban: undefined,
21
+ bic: undefined,
22
+ };
23
+ function configWith(allowedTransactions) {
24
+ return FinTSConfig.fromBankingInformation('product', '1.0', {
25
+ systemId: 'SYSTEM01',
26
+ bpd: {
27
+ version: 1,
28
+ url: 'https://bank.example.com/fints',
29
+ countryCode: 280,
30
+ bankId: '10020030',
31
+ bankName: 'Example Bank',
32
+ allowedTransactions,
33
+ maxTransactionsPerMessage: 1,
34
+ supportedLanguages: [Language.German],
35
+ supportedHbciVersions: [300],
36
+ supportedTanMethods: [],
37
+ availableTanMethodIds: [],
38
+ },
39
+ upd: { version: 1, usage: 0, bankAccounts: [account, depot] },
40
+ bankMessages: [],
41
+ });
42
+ }
43
+ const hispas = (nationalAccountAllowed) => ({
44
+ transId: 'HKSPA',
45
+ versions: [1],
46
+ tanRequired: false,
47
+ params: {
48
+ individualAccountRetrievalAllowed: false,
49
+ nationalAccountAllowed,
50
+ structuredPurposeAllowed: false,
51
+ },
52
+ });
53
+ describe('nationalAccount', () => {
54
+ it('carries the three fields the data group has, and nothing else', () => {
55
+ expect(nationalAccount(account)).toEqual({
56
+ accountNumber: '1234567890',
57
+ subAccountId: 'Girokonto',
58
+ bank: { country: 280, bankId: '10020030' },
59
+ });
60
+ });
61
+ });
62
+ describe('internationalAccount', () => {
63
+ it('leaves the national fields out when the bank forbids them', () => {
64
+ const descriptor = internationalAccount(configWith([hispas(false)]), account);
65
+ expect(descriptor).toEqual({ iban: 'DE89370400440532013000', bic: 'BANKDEFFXXX' });
66
+ });
67
+ it('includes the national fields when the bank allows them', () => {
68
+ const descriptor = internationalAccount(configWith([hispas(true)]), account);
69
+ expect(descriptor).toEqual({
70
+ iban: 'DE89370400440532013000',
71
+ bic: 'BANKDEFFXXX',
72
+ accountNumber: '1234567890',
73
+ subAccountId: 'Girokonto',
74
+ bank: { country: 280, bankId: '10020030' },
75
+ });
76
+ });
77
+ it('keeps them when the bank announces no HISPAS at all', () => {
78
+ // Silence is not a refusal. Only a bank that says false gets the shorter form,
79
+ // so a bank working today cannot regress on a rule it never stated — see #25,
80
+ // where Postbank required the national fields for identification.
81
+ const descriptor = internationalAccount(configWith([]), account);
82
+ expect(descriptor).toEqual({
83
+ iban: 'DE89370400440532013000',
84
+ bic: 'BANKDEFFXXX',
85
+ accountNumber: '1234567890',
86
+ subAccountId: 'Girokonto',
87
+ bank: { country: 280, bankId: '10020030' },
88
+ });
89
+ });
90
+ it('keeps the national fields for an account without an IBAN, whatever the flag says', () => {
91
+ // A securities account typically has none, and nothing else identifies it.
92
+ const descriptor = internationalAccount(configWith([hispas(false)]), depot);
93
+ expect(descriptor).toEqual({
94
+ accountNumber: '9876543210',
95
+ subAccountId: 'Girokonto',
96
+ bank: { country: 280, bankId: '10020030' },
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,126 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AccountType, describeAccount } from '../bankAccount.js';
3
+ import { Language } from '../codes.js';
4
+ import { FinTSConfig } from '../config.js';
5
+ import { HKSAL } from '../segments/HKSAL.js';
6
+ import { HKWPD } from '../segments/HKWPD.js';
7
+ // A bank that gives a securities account and the current account it settles through
8
+ // the same number, distinguishing them only by sub-account id. That is within the
9
+ // specification: FinTS identifies an account by both together.
10
+ const checkingAccount = {
11
+ accountNumber: '1234567890',
12
+ subAccountId: 'Girokonto',
13
+ bank: { country: 280, bankId: '10020030' },
14
+ iban: 'DE89370400440532013000',
15
+ bic: 'BANKDEFFXXX',
16
+ customerId: 'customer1',
17
+ accountType: AccountType.Miscellaneous,
18
+ currency: 'EUR',
19
+ holder1: 'Test User',
20
+ allowedTransactions: [{ transId: HKSAL.Id, numSignatures: 0 }],
21
+ };
22
+ const securitiesAccount = {
23
+ ...checkingAccount,
24
+ subAccountId: 'Depot',
25
+ iban: undefined,
26
+ bic: undefined,
27
+ allowedTransactions: [{ transId: HKWPD.Id, numSignatures: 0 }],
28
+ };
29
+ const singleAccount = {
30
+ ...checkingAccount,
31
+ accountNumber: '5555555555',
32
+ subAccountId: undefined,
33
+ };
34
+ function configWith(accounts) {
35
+ return FinTSConfig.fromBankingInformation('product', '1.0', {
36
+ systemId: 'SYSTEM01',
37
+ bpd: {
38
+ version: 1,
39
+ url: 'https://bank.example.com/fints',
40
+ countryCode: 280,
41
+ bankId: '10020030',
42
+ bankName: 'Example Bank',
43
+ allowedTransactions: [],
44
+ maxTransactionsPerMessage: 1,
45
+ supportedLanguages: [Language.German],
46
+ supportedHbciVersions: [300],
47
+ supportedTanMethods: [],
48
+ availableTanMethodIds: [],
49
+ },
50
+ upd: { version: 1, usage: 0, bankAccounts: accounts },
51
+ bankMessages: [],
52
+ });
53
+ }
54
+ describe('addressing an account by number', () => {
55
+ it('resolves a number that only one account has', () => {
56
+ const config = configWith([checkingAccount, singleAccount]);
57
+ expect(config.getBankAccount('5555555555').subAccountId).toBeUndefined();
58
+ });
59
+ it('refuses a number two accounts share, instead of picking one', () => {
60
+ // Picking the first is what makes the failure invisible: a balance comes back,
61
+ // it is the other account's, and nothing in the response says so.
62
+ const config = configWith([checkingAccount, securitiesAccount]);
63
+ expect(() => config.getBankAccount('1234567890')).toThrow(/not unique/);
64
+ });
65
+ it('names the sub-account ids, so the caller can tell them apart', () => {
66
+ const config = configWith([checkingAccount, securitiesAccount]);
67
+ expect(() => config.getBankAccount('1234567890')).toThrow(/Girokonto, Depot/);
68
+ });
69
+ it('still says so when the number matches nothing', () => {
70
+ expect(() => configWith([checkingAccount]).getBankAccount('0000000000')).toThrow(/not found in UPD/);
71
+ });
72
+ });
73
+ describe('addressing an account by the account itself', () => {
74
+ it('reaches the one a shared number cannot', () => {
75
+ const config = configWith([checkingAccount, securitiesAccount]);
76
+ expect(config.getBankAccount(securitiesAccount).subAccountId).toBe('Depot');
77
+ expect(config.getBankAccount(checkingAccount).subAccountId).toBe('Girokonto');
78
+ });
79
+ it('decides what that account may do, not what the other one may', () => {
80
+ const config = configWith([checkingAccount, securitiesAccount]);
81
+ expect(config.isAccountTransactionSupported(securitiesAccount, HKWPD.Id)).toBe(true);
82
+ expect(config.isAccountTransactionSupported(checkingAccount, HKWPD.Id)).toBe(false);
83
+ expect(config.isAccountTransactionSupported(checkingAccount, HKSAL.Id)).toBe(true);
84
+ });
85
+ it('resolves against the UPD rather than trusting what it was handed', () => {
86
+ // A caller may hold an account from a persisted earlier session. The entry the
87
+ // bank sent this time is the one carrying the current allowed transactions.
88
+ const staleAccount = { ...securitiesAccount, allowedTransactions: [] };
89
+ const config = configWith([checkingAccount, securitiesAccount]);
90
+ expect(config.isAccountTransactionSupported(staleAccount, HKWPD.Id)).toBe(true);
91
+ });
92
+ it('refuses an account the bank did not report', () => {
93
+ const config = configWith([checkingAccount]);
94
+ const unreportedAccount = { ...checkingAccount, subAccountId: 'Sparkonto' };
95
+ expect(() => config.getBankAccount(unreportedAccount)).toThrow(/not found in UPD/);
96
+ });
97
+ });
98
+ describe('naming an account in an error', () => {
99
+ it('reads as the number alone where that is all there is', () => {
100
+ expect(describeAccount('1234567890')).toBe('1234567890');
101
+ expect(describeAccount(singleAccount)).toBe('5555555555');
102
+ });
103
+ it('adds the sub-account id where there is one', () => {
104
+ // Otherwise an account passed as an object prints as [object Object].
105
+ expect(describeAccount(securitiesAccount)).toBe('1234567890 (Depot)');
106
+ });
107
+ });
108
+ describe('matching an account the bank itself named', () => {
109
+ it('uses the sub-account id where the bank repeated it', () => {
110
+ const config = configWith([checkingAccount, securitiesAccount]);
111
+ expect(config.matchBankAccount({ accountNumber: '1234567890', subAccountId: 'Depot' })?.subAccountId).toBe('Depot');
112
+ });
113
+ it('still finds an account whose number only it has, sub-account id or not', () => {
114
+ // Banks are not consistent about repeating it, and a number only one account
115
+ // has identifies that account either way.
116
+ const config = configWith([checkingAccount, singleAccount]);
117
+ expect(config.matchBankAccount({ accountNumber: '5555555555' })?.accountNumber).toBe('5555555555');
118
+ });
119
+ it('gives up quietly where it cannot tell, rather than throwing', () => {
120
+ // This runs for entries the bank supplied — HISPA travels with every dialog —
121
+ // so throwing here would fail every request at a bank that shares numbers,
122
+ // before any of them reached its order. That is exactly what happened once.
123
+ const config = configWith([checkingAccount, securitiesAccount]);
124
+ expect(config.matchBankAccount({ accountNumber: '1234567890' })).toBeUndefined();
125
+ });
126
+ });
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AccountType } from '../bankAccount.js';
3
+ import { Language } from '../codes.js';
4
+ import { FinTSConfig } from '../config.js';
5
+ import { BalanceInteraction } from '../interactions/balanceInteraction.js';
6
+ import { ElectronicStatementInteraction } from '../interactions/electronicStatementInteraction.js';
7
+ import { PortfolioInteraction } from '../interactions/portfolioInteraction.js';
8
+ import { StatementInteractionCAMT } from '../interactions/statementInteractionCAMT.js';
9
+ import { StatementInteractionMT940 } from '../interactions/statementInteractionMT940.js';
10
+ import { registerSegments } from '../segments/registry.js';
11
+ registerSegments();
12
+ // The layer between "the segment encodes correctly" and "the client picks the right
13
+ // interaction": what an interaction puts into the account connection. Nothing
14
+ // covered it before — the segment tests build their account by hand, and the client
15
+ // tests mock `Dialog.start`, which throws the built request away.
16
+ const GIRO = '1234567890';
17
+ const DEPOT = '9876543210';
18
+ const BANK = { country: 280, bankId: '10020030' };
19
+ const IBAN = 'DE89370400440532013000';
20
+ const konto = (accountNumber, transIds, iban) => ({
21
+ accountNumber,
22
+ subAccountId: 'Girokonto',
23
+ bank: BANK,
24
+ iban,
25
+ bic: iban ? 'BANKDEFFXXX' : undefined,
26
+ customerId: 'customer1',
27
+ accountType: AccountType.CheckingAccount,
28
+ currency: 'EUR',
29
+ holder1: 'Test User',
30
+ allowedTransactions: transIds.map((transId) => ({ transId, numSignatures: 1 })),
31
+ });
32
+ function configFor(transactions, nationalAccountAllowed) {
33
+ const allowedTransactions = Object.entries(transactions).map(([transId, versions]) => ({ transId, versions, tanRequired: false }));
34
+ if (nationalAccountAllowed !== undefined) {
35
+ allowedTransactions.push({
36
+ transId: 'HKSPA',
37
+ versions: [1],
38
+ tanRequired: false,
39
+ params: {
40
+ individualAccountRetrievalAllowed: false,
41
+ nationalAccountAllowed,
42
+ structuredPurposeAllowed: false,
43
+ },
44
+ });
45
+ }
46
+ return FinTSConfig.fromBankingInformation('product', '1.0', {
47
+ systemId: 'SYSTEM01',
48
+ bpd: {
49
+ version: 1,
50
+ url: 'https://bank.example.com/fints',
51
+ countryCode: 280,
52
+ bankId: '10020030',
53
+ bankName: 'Example Bank',
54
+ allowedTransactions,
55
+ maxTransactionsPerMessage: 1,
56
+ supportedLanguages: [Language.German],
57
+ supportedHbciVersions: [300],
58
+ supportedTanMethods: [],
59
+ availableTanMethodIds: [],
60
+ },
61
+ upd: {
62
+ version: 1,
63
+ usage: 0,
64
+ bankAccounts: [
65
+ konto(GIRO, ['HKSAL', 'HKKAZ', 'HKCAZ', 'HKEKA'], IBAN),
66
+ konto(DEPOT, ['HKWPD'], undefined),
67
+ ],
68
+ },
69
+ bankMessages: [],
70
+ });
71
+ }
72
+ // biome-ignore lint/suspicious/noExplicitAny: reading one field off a built segment
73
+ const account = (segment) => segment.account;
74
+ describe('HKCAZ — international at every version', () => {
75
+ it('sends IBAN and BIC only when the bank forbids the national fields', () => {
76
+ const config = configFor({ HKCAZ: [1] }, false);
77
+ const [hkcaz] = new StatementInteractionCAMT(GIRO).createSegments(config);
78
+ expect(account(hkcaz)).toEqual({ iban: IBAN, bic: 'BANKDEFFXXX' });
79
+ });
80
+ it('sends both halves when the bank allows them', () => {
81
+ const config = configFor({ HKCAZ: [1] }, true);
82
+ const [hkcaz] = new StatementInteractionCAMT(GIRO).createSegments(config);
83
+ expect(account(hkcaz)).toEqual({
84
+ iban: IBAN,
85
+ bic: 'BANKDEFFXXX',
86
+ accountNumber: GIRO,
87
+ subAccountId: 'Girokonto',
88
+ bank: BANK,
89
+ });
90
+ });
91
+ });
92
+ describe('HKSAL — national up to version 6, international from 7', () => {
93
+ it('sends the national form at version 6, with no IBAN', () => {
94
+ const [hksal] = new BalanceInteraction(GIRO).createSegments(configFor({ HKSAL: [6] }, false));
95
+ expect(account(hksal)).toEqual({ accountNumber: GIRO, subAccountId: 'Girokonto', bank: BANK });
96
+ });
97
+ it('honours the flag at version 7 instead of filling both halves', () => {
98
+ const [hksal] = new BalanceInteraction(GIRO).createSegments(configFor({ HKSAL: [7] }, false));
99
+ expect(account(hksal)).toEqual({ iban: IBAN, bic: 'BANKDEFFXXX' });
100
+ });
101
+ });
102
+ describe('HKKAZ — national up to version 6, international from 7', () => {
103
+ it('sends the national form at version 6', () => {
104
+ const [hkkaz] = new StatementInteractionMT940(GIRO).createSegments(configFor({ HKKAZ: [6] }, false));
105
+ expect(account(hkkaz)).toEqual({ accountNumber: GIRO, subAccountId: 'Girokonto', bank: BANK });
106
+ });
107
+ it('honours the flag at version 7', () => {
108
+ const [hkkaz] = new StatementInteractionMT940(GIRO).createSegments(configFor({ HKKAZ: [7] }, false));
109
+ expect(account(hkkaz)).toEqual({ iban: IBAN, bic: 'BANKDEFFXXX' });
110
+ });
111
+ });
112
+ describe('HKEKA — national up to version 3, international from 4', () => {
113
+ it('sends the national form at version 3', () => {
114
+ const [hkeka] = new ElectronicStatementInteraction(GIRO, {}).createSegments(configFor({ HKEKA: [3] }, false));
115
+ expect(account(hkeka)).toEqual({ accountNumber: GIRO, subAccountId: 'Girokonto', bank: BANK });
116
+ });
117
+ it('honours the flag at version 4', () => {
118
+ const [hkeka] = new ElectronicStatementInteraction(GIRO, {}).createSegments(configFor({ HKEKA: [4] }, false));
119
+ expect(account(hkeka)).toEqual({ iban: IBAN, bic: 'BANKDEFFXXX' });
120
+ });
121
+ });
122
+ describe('HKWPD — national at every version', () => {
123
+ it('sends the national form, and the depot has no IBAN to send anyway', () => {
124
+ const [hkwpd] = new PortfolioInteraction(DEPOT).createSegments(configFor({ HKWPD: [5] }, false));
125
+ // biome-ignore lint/suspicious/noExplicitAny: reading one field off a built segment
126
+ expect(hkwpd.depot).toEqual({
127
+ accountNumber: DEPOT,
128
+ subAccountId: 'Girokonto',
129
+ bank: BANK,
130
+ });
131
+ });
132
+ });
133
+ // The two banks that pulled this in opposite directions. #19/#20 reduced the CAMT
134
+ // descriptor to IBAN and BIC because comdirect rejects anything more; #25 reported
135
+ // Postbank answering "Angaben zur nationalen Kontoverbindung für Identifikation
136
+ // erforderlich", and the reduction was reverted. Neither bank was wrong, and neither
137
+ // fix could hold, because the choice was hard-coded either way. It is data now.
138
+ describe('the two banks that pulled this in opposite directions', () => {
139
+ it('a bank refusing the national fields gets IBAN and BIC only', () => {
140
+ const [hkcaz] = new StatementInteractionCAMT(GIRO).createSegments(configFor({ HKCAZ: [1] }, false));
141
+ expect(account(hkcaz)).toEqual({ iban: IBAN, bic: 'BANKDEFFXXX' });
142
+ });
143
+ it('a bank that says nothing keeps them, so nothing working today regresses', () => {
144
+ const [hkcaz] = new StatementInteractionCAMT(GIRO).createSegments(configFor({ HKCAZ: [1] }));
145
+ expect(account(hkcaz)).toEqual({
146
+ iban: IBAN,
147
+ bic: 'BANKDEFFXXX',
148
+ accountNumber: GIRO,
149
+ subAccountId: 'Girokonto',
150
+ bank: BANK,
151
+ });
152
+ });
153
+ });
@@ -77,4 +77,22 @@ describe('CustomerMessage', () => {
77
77
  const encodedMessage = customerMessage.encode();
78
78
  const _message = Message.decode(encodedMessage);
79
79
  });
80
+ it('encrypts with the security profile version of the signature (two-step: PIN:2)', () => {
81
+ const customerMessage = new CustomerMessage('0', 1);
82
+ customerMessage.addSegment(hkidn);
83
+ customerMessage.addSegment(hkvvb);
84
+ customerMessage.sign(280, '12030000', '12345678', '123', '0', 900, '12345');
85
+ const encodedMessage = customerMessage.encode();
86
+ expect(encodedMessage).toContain("'HNVSK:998:3+PIN:2+998+");
87
+ expect(encodedMessage).toContain('HNSHK:2:4+PIN:2+900+');
88
+ });
89
+ it('encrypts with the security profile version of the signature (one-step: PIN:1)', () => {
90
+ const customerMessage = new CustomerMessage('0', 1);
91
+ customerMessage.addSegment(hkidn);
92
+ customerMessage.addSegment(hkvvb);
93
+ customerMessage.sign(280, '12030000', '12345678', '123', '0');
94
+ const encodedMessage = customerMessage.encode();
95
+ expect(encodedMessage).toContain("'HNVSK:998:3+PIN:1+998+");
96
+ expect(encodedMessage).toContain('HNSHK:2:4+PIN:1+999+');
97
+ });
80
98
  });
@@ -0,0 +1,49 @@
1
+ import type { BankAccount } from './bankAccount.js';
2
+ import type { FinTSConfig } from './config.js';
3
+ import type { Account } from './dataGroups/Account.js';
4
+ import type { InternationalAccount } from './dataGroups/InternationalAccount.js';
5
+ /**
6
+ * Builds the account connection ("Kontoverbindung") a segment carries.
7
+ *
8
+ * FinTS has two forms, and segments pick one by version: the national form (KTV,
9
+ * account number + sub-account + bank) and the international one (KTI, which adds
10
+ * IBAN and BIC and makes every field optional). The international form allows the
11
+ * national fields to be present as well, but only where the bank permits it — and
12
+ * the bank says so in the HISPAS parameters, in `nationalAccountAllowed`.
13
+ *
14
+ * Filling both halves regardless is rejected by banks that set the flag to false.
15
+ * Measured at comdirect (BLZ 2004xxxx), same account, same range, same session:
16
+ *
17
+ * IBAN + BIC + number + sub-account + bank → 3010 "Kontonummer ist ungültig", 0 statements
18
+ * IBAN + BIC → 0020 "Auftrag ausgeführt", 19 statements
19
+ * IBAN → 0020 "Auftrag ausgeführt", 19 statements
20
+ * number + sub-account + bank → 3010 "Kontonummer ist ungültig", 0 statements
21
+ *
22
+ * The last line is why this is a rule about the national *fields* rather than about
23
+ * the combination: the bank rejects them in a KTI even when no IBAN accompanies
24
+ * them.
25
+ */
26
+ /**
27
+ * The national form. Built field by field rather than by spreading the account and
28
+ * blanking what does not belong: the data group has exactly these three fields, and
29
+ * saying so is clearer than relying on the encoder to ignore the rest.
30
+ */
31
+ export declare function nationalAccount(account: BankAccount): Account;
32
+ /**
33
+ * The international form. IBAN and BIC always; the national fields only where the
34
+ * bank's HISPAS parameters allow them.
35
+ *
36
+ * An account without an IBAN — a securities account, typically — has nothing else
37
+ * to identify it with, so it keeps the national fields whatever the flag says. A
38
+ * request the bank refuses is more useful than one it cannot resolve at all.
39
+ *
40
+ * A bank that announces no HISPAS at all keeps both halves, exactly as before. The
41
+ * specification would read absent permission as no permission, but this library has
42
+ * already been round that loop: #20 reduced the CAMT descriptor to IBAN and BIC for
43
+ * comdirect, #25 reported Postbank answering "Angaben zur nationalen Kontoverbindung
44
+ * für Identifikation erforderlich", and the reduction was reverted. Only a bank that
45
+ * says `false` gets the shorter form, so no bank that works today can regress on a
46
+ * rule it never stated.
47
+ */
48
+ export declare function internationalAccount(config: FinTSConfig, account: BankAccount): InternationalAccount;
49
+ //# sourceMappingURL=accountDescriptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accountDescriptor.d.ts","sourceRoot":"","sources":["../../src/accountDescriptor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAIjF;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAM7D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,WAAW,GAClB,oBAAoB,CAgBtB"}
@@ -22,5 +22,18 @@ export type BankAccount = SepaAccount & {
22
22
  limit?: AccountLimit;
23
23
  allowedTransactions?: AllowedTransactions[];
24
24
  };
25
+ /**
26
+ * How a caller names an account.
27
+ *
28
+ * An account number is not by itself unique: FinTS identifies an account by number
29
+ * *and* sub-account id together, and banks use that — a securities account and the
30
+ * current account it settles through commonly share a number and differ only in the
31
+ * sub-account id. Where that happens, a number alone cannot say which one is meant,
32
+ * so the account itself can be passed instead. Take it from
33
+ * `config.bankingInformation.upd.bankAccounts`.
34
+ */
35
+ export type AccountRef = string | BankAccount;
36
+ /** How an account reference reads in an error message. */
37
+ export declare function describeAccount(account: AccountRef): string;
25
38
  export declare function finTsAccountTypeToEnum(accountType: number): AccountType;
26
39
  //# sourceMappingURL=bankAccount.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bankAccount.d.ts","sourceRoot":"","sources":["../../src/bankAccount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE7E,oBAAY,WAAW;IACtB,eAAe,oBAAoB;IACnC,cAAc,mBAAmB;IACjC,mBAAmB,wBAAwB;IAC3C,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;IAC3C,iBAAiB,sBAAsB;IACvC,qBAAqB,0BAA0B;IAC/C,mBAAmB,wBAAwB;IAC3C,eAAe,oBAAoB;IACnC,aAAa,kBAAkB;CAC/B;AAED,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,mBAAmB,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAC5C,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAWvE"}
1
+ {"version":3,"file":"bankAccount.d.ts","sourceRoot":"","sources":["../../src/bankAccount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE7E,oBAAY,WAAW;IACtB,eAAe,oBAAoB;IACnC,cAAc,mBAAmB;IACjC,mBAAmB,wBAAwB;IAC3C,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;IAC3C,iBAAiB,sBAAsB;IACvC,qBAAqB,0BAA0B;IAC/C,mBAAmB,wBAAwB;IAC3C,eAAe,oBAAoB;IACnC,aAAa,kBAAkB;CAC/B;AAED,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,mBAAmB,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAC5C,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,WAAW,CAAC;AAE9C,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAK3D;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAWvE"}