lib-fints 1.4.8 → 1.5.1

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 (80) hide show
  1. package/README.md +21 -15
  2. package/dist/accountDescriptor.js +65 -0
  3. package/dist/bankAccount.js +8 -0
  4. package/dist/client.js +71 -33
  5. package/dist/config.js +57 -10
  6. package/dist/dataElements/Binary.js +18 -1
  7. package/dist/dialog.js +52 -31
  8. package/dist/electronicStatement.js +1 -0
  9. package/dist/httpClient.js +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/interactions/balanceInteraction.js +10 -8
  12. package/dist/interactions/creditcardStatementInteraction.js +17 -8
  13. package/dist/interactions/electronicStatementInteraction.js +89 -0
  14. package/dist/interactions/portfolioInteraction.js +19 -11
  15. package/dist/interactions/sepaAccountInteraction.js +11 -8
  16. package/dist/interactions/statementInteractionCAMT.js +14 -8
  17. package/dist/interactions/statementInteractionMT940.js +17 -9
  18. package/dist/message.js +10 -2
  19. package/dist/segments/HIEKA.js +45 -0
  20. package/dist/segments/HIEKAS.js +18 -0
  21. package/dist/segments/HKEKA.js +43 -0
  22. package/dist/segments/HKKAZ.js +1 -1
  23. package/dist/segments/registry.js +6 -0
  24. package/dist/tests/HKEKA.test.js +198 -0
  25. package/dist/tests/accountDescriptor.test.js +99 -0
  26. package/dist/tests/accountReference.test.js +126 -0
  27. package/dist/tests/interactionAccountDescriptor.test.js +153 -0
  28. package/dist/tests/message.test.js +18 -0
  29. package/dist/tests/partedResponse.test.js +181 -0
  30. package/dist/types/accountDescriptor.d.ts +49 -0
  31. package/dist/types/accountDescriptor.d.ts.map +1 -0
  32. package/dist/types/bankAccount.d.ts +13 -0
  33. package/dist/types/bankAccount.d.ts.map +1 -1
  34. package/dist/types/client.d.ts +45 -16
  35. package/dist/types/client.d.ts.map +1 -1
  36. package/dist/types/config.d.ts +27 -6
  37. package/dist/types/config.d.ts.map +1 -1
  38. package/dist/types/dataElements/Binary.d.ts +7 -0
  39. package/dist/types/dataElements/Binary.d.ts.map +1 -1
  40. package/dist/types/dialog.d.ts +14 -0
  41. package/dist/types/dialog.d.ts.map +1 -1
  42. package/dist/types/electronicStatement.d.ts +40 -0
  43. package/dist/types/electronicStatement.d.ts.map +1 -0
  44. package/dist/types/index.d.ts +3 -0
  45. package/dist/types/index.d.ts.map +1 -1
  46. package/dist/types/interactions/balanceInteraction.d.ts +3 -2
  47. package/dist/types/interactions/balanceInteraction.d.ts.map +1 -1
  48. package/dist/types/interactions/creditcardStatementInteraction.d.ts +3 -2
  49. package/dist/types/interactions/creditcardStatementInteraction.d.ts.map +1 -1
  50. package/dist/types/interactions/electronicStatementInteraction.d.ts +34 -0
  51. package/dist/types/interactions/electronicStatementInteraction.d.ts.map +1 -0
  52. package/dist/types/interactions/portfolioInteraction.d.ts +3 -2
  53. package/dist/types/interactions/portfolioInteraction.d.ts.map +1 -1
  54. package/dist/types/interactions/sepaAccountInteraction.d.ts +3 -2
  55. package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -1
  56. package/dist/types/interactions/statementInteractionCAMT.d.ts +3 -2
  57. package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -1
  58. package/dist/types/interactions/statementInteractionMT940.d.ts +3 -2
  59. package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -1
  60. package/dist/types/message.d.ts.map +1 -1
  61. package/dist/types/segments/HIEKA.d.ts +52 -0
  62. package/dist/types/segments/HIEKA.d.ts.map +1 -0
  63. package/dist/types/segments/HIEKAS.d.ts +21 -0
  64. package/dist/types/segments/HIEKAS.d.ts.map +1 -0
  65. package/dist/types/segments/HKEKA.d.ts +54 -0
  66. package/dist/types/segments/HKEKA.d.ts.map +1 -0
  67. package/dist/types/segments/HKKAZ.d.ts +2 -2
  68. package/dist/types/segments/HKKAZ.d.ts.map +1 -1
  69. package/dist/types/segments/registry.d.ts.map +1 -1
  70. package/dist/types/tests/HKEKA.test.d.ts +2 -0
  71. package/dist/types/tests/HKEKA.test.d.ts.map +1 -0
  72. package/dist/types/tests/accountDescriptor.test.d.ts +2 -0
  73. package/dist/types/tests/accountDescriptor.test.d.ts.map +1 -0
  74. package/dist/types/tests/accountReference.test.d.ts +2 -0
  75. package/dist/types/tests/accountReference.test.d.ts.map +1 -0
  76. package/dist/types/tests/interactionAccountDescriptor.test.d.ts +2 -0
  77. package/dist/types/tests/interactionAccountDescriptor.test.d.ts.map +1 -0
  78. package/dist/types/tests/partedResponse.test.d.ts +2 -0
  79. package/dist/types/tests/partedResponse.test.d.ts.map +1 -0
  80. package/package.json +2 -2
package/README.md CHANGED
@@ -85,14 +85,17 @@ Finally you can start fetching balances or statements:
85
85
  // for simplicity, use the first account
86
86
  const account = syncResponse.bankingInformation.upd.bankAccounts[0];
87
87
 
88
+ // Account-specific methods accept either an account number or a BankAccount.
89
+ // Pass the account object when multiple accounts share the same number.
90
+
88
91
  // fetch the current balance
89
- const balanceResponse = await client.getAccountBalance(account.accountNumber);
92
+ const balanceResponse = await client.getAccountBalance(account);
90
93
 
91
94
  // fetch all available statements
92
- const statementResponse = await client.getAccountStatements(account.accountNumber);
95
+ const statementResponse = await client.getAccountStatements(account);
93
96
 
94
97
  // or fetch portfolio from a securities account
95
- client.getPortfolio(account.accountNumber);
98
+ client.getPortfolio(account);
96
99
  ```
97
100
 
98
101
  These are only the most basic steps needed to retrieve information from the bank. There are still some unanswered questions like "how to handle TANs" or "how to avoid synchronizations every time you start a new session". These are explained in the corresponding sections below.
@@ -110,7 +113,7 @@ const rl = readline.createInterface({
110
113
  output: process.stdout,
111
114
  });
112
115
 
113
- let response = await client.getAccountStatements(account.accountNumber);
116
+ let response = await client.getAccountStatements(account);
114
117
 
115
118
  if (!response.success) {
116
119
  return;
@@ -197,10 +200,11 @@ The following table shows all transactions supported by the FinTSClient interfac
197
200
  | Transaction | Method | Description | FinTS Segment(s) | TAN Support | Account-Specific |
198
201
  | -------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------- | ----------- | ---------------- |
199
202
  | **Synchronization** | `synchronize()` | Synchronizes bank and account information, updating config.bankingInformation | HKIDN, HKVVB, HKSYN, HKTAB | ✓ | ❌ |
200
- | **Account Balance** | `getAccountBalance(accountNumber)` | Fetches the current balance for a specific account | HKSAL | ✓ | ✓ |
201
- | **Account Statements** | `getAccountStatements(accountNumber, from?, to?)` | Fetches account transactions/statements for a date range (MT940 or CAMT format) | HKKAZ, HKCAZ | ✓ | ✓ |
202
- | **Portfolio** | `getPortfolio(accountNumber, currency?, priceQuality?, maxEntries?)` | Fetches securities portfolio information for depot accounts | HKWPD | ✓ | ✓ |
203
- | **Credit Card Statements** | `getCreditCardStatements(accountNumber, from?)` | Fetches credit card statements for credit card accounts | DKKKU | ✓ | ✓ |
203
+ | **Account Balance** | `getAccountBalance(account: AccountRef)` | Fetches the current balance for a specific account | HKSAL | ✓ | ✓ |
204
+ | **Account Statements** | `getAccountStatements(account: AccountRef, from?, to?)` | Fetches account transactions/statements for a date range (MT940 or CAMT format) | HKKAZ, HKCAZ | ✓ | ✓ |
205
+ | **Portfolio** | `getPortfolio(account: AccountRef, currency?, priceQuality?, maxEntries?)` | Fetches securities portfolio information for depot accounts | HKWPD | ✓ | ✓ |
206
+ | **Credit Card Statements** | `getCreditCardStatements(account: AccountRef, from?)` | Fetches credit card statements for credit card accounts | DKKKU | ✓ | ✓ |
207
+ | **Electronic Statements** | `getElectronicStatements(account: AccountRef, options?)` | Fetches the statement document from the electronic mailbox, usually a PDF | HKEKA | ✓ | ✓ |
204
208
  | **TAN Method Selection** | `selectTanMethod(tanMethodId)` | Selects a TAN method by ID from available methods | - | ❌ | ❌ |
205
209
  | **TAN Media Selection** | `selectTanMedia(tanMediaName)` | Selects a specific TAN media device by name | - | ❌ | ❌ |
206
210
 
@@ -210,10 +214,11 @@ For each account-specific transaction, the client provides corresponding `can*`
210
214
 
211
215
  | Support Check Method | Purpose |
212
216
  | -------------------------------------------- | --------------------------------------------------------------- |
213
- | `canGetAccountBalance(accountNumber?)` | Checks if account balance fetching is supported |
214
- | `canGetAccountStatements(accountNumber?)` | Checks if account statements fetching is supported (MT940/CAMT) |
215
- | `canGetPortfolio(accountNumber?)` | Checks if portfolio information fetching is supported |
216
- | `canGetCreditCardStatements(accountNumber?)` | Checks if credit card statements fetching is supported |
217
+ | `canGetAccountBalance(account?: AccountRef)` | Checks if account balance fetching is supported |
218
+ | `canGetAccountStatements(account?: AccountRef)` | Checks if account statements fetching is supported (MT940/CAMT) |
219
+ | `canGetPortfolio(account?: AccountRef)` | Checks if portfolio information fetching is supported |
220
+ | `canGetCreditCardStatements(account?: AccountRef)` | Checks if credit card statements fetching is supported |
221
+ | `canGetElectronicStatements(account?: AccountRef)` | Checks if electronic account statements fetching is supported |
217
222
 
218
223
  ### Transaction Parameters
219
224
 
@@ -244,12 +249,12 @@ if (config.isTransactionSupported('HKWPD')) {
244
249
  }
245
250
  ```
246
251
 
247
- #### `config.isAccountTransactionSupported(accountNumber: string, transId: string): boolean`
252
+ #### `config.isAccountTransactionSupported(account: AccountRef, transId: string): boolean`
248
253
 
249
254
  Checks whether a specific transaction type is supported for a particular account.
250
255
 
251
256
  ```typescript
252
- if (config.isAccountTransactionSupported('1234567890', 'HKWPD')) {
257
+ if (config.isAccountTransactionSupported(account, 'HKWPD')) {
253
258
  console.log('Account supports portfolio requests');
254
259
  }
255
260
  ```
@@ -278,6 +283,7 @@ Implementing further transactions should be straight forward and contributions a
278
283
 
279
284
  ### Successfully tested with the following banks
280
285
 
286
+ - comdirect
281
287
  - DKB
282
288
  - ING-DiBa
283
289
  - Renault Bank Direkt
@@ -299,7 +305,7 @@ As this is a free-time project, a lot of things are still remaining which could
299
305
 
300
306
  ## License
301
307
 
302
- This project is licensed under the LGPL 3.0 License - see the [LICENSE](LICENSE) file for details
308
+ This project is licensed under the LGPL-2.1-or-later License - see the [LICENSE](LICENSE) file for details
303
309
 
304
310
  ## References
305
311
 
@@ -0,0 +1,65 @@
1
+ import { HKSPA } from './segments/HKSPA.js';
2
+ /**
3
+ * Builds the account connection ("Kontoverbindung") a segment carries.
4
+ *
5
+ * FinTS has two forms, and segments pick one by version: the national form (KTV,
6
+ * account number + sub-account + bank) and the international one (KTI, which adds
7
+ * IBAN and BIC and makes every field optional). The international form allows the
8
+ * national fields to be present as well, but only where the bank permits it — and
9
+ * the bank says so in the HISPAS parameters, in `nationalAccountAllowed`.
10
+ *
11
+ * Filling both halves regardless is rejected by banks that set the flag to false.
12
+ * Measured at comdirect (BLZ 2004xxxx), same account, same range, same session:
13
+ *
14
+ * IBAN + BIC + number + sub-account + bank → 3010 "Kontonummer ist ungültig", 0 statements
15
+ * IBAN + BIC → 0020 "Auftrag ausgeführt", 19 statements
16
+ * IBAN → 0020 "Auftrag ausgeführt", 19 statements
17
+ * number + sub-account + bank → 3010 "Kontonummer ist ungültig", 0 statements
18
+ *
19
+ * The last line is why this is a rule about the national *fields* rather than about
20
+ * the combination: the bank rejects them in a KTI even when no IBAN accompanies
21
+ * them.
22
+ */
23
+ /**
24
+ * The national form. Built field by field rather than by spreading the account and
25
+ * blanking what does not belong: the data group has exactly these three fields, and
26
+ * saying so is clearer than relying on the encoder to ignore the rest.
27
+ */
28
+ export function nationalAccount(account) {
29
+ return {
30
+ accountNumber: account.accountNumber,
31
+ subAccountId: account.subAccountId,
32
+ bank: account.bank,
33
+ };
34
+ }
35
+ /**
36
+ * The international form. IBAN and BIC always; the national fields only where the
37
+ * bank's HISPAS parameters allow them.
38
+ *
39
+ * An account without an IBAN — a securities account, typically — has nothing else
40
+ * to identify it with, so it keeps the national fields whatever the flag says. A
41
+ * request the bank refuses is more useful than one it cannot resolve at all.
42
+ *
43
+ * A bank that announces no HISPAS at all keeps both halves, exactly as before. The
44
+ * specification would read absent permission as no permission, but this library has
45
+ * already been round that loop: #20 reduced the CAMT descriptor to IBAN and BIC for
46
+ * comdirect, #25 reported Postbank answering "Angaben zur nationalen Kontoverbindung
47
+ * für Identifikation erforderlich", and the reduction was reverted. Only a bank that
48
+ * says `false` gets the shorter form, so no bank that works today can regress on a
49
+ * rule it never stated.
50
+ */
51
+ export function internationalAccount(config, account) {
52
+ if (!account.iban) {
53
+ return nationalAccount(account);
54
+ }
55
+ const hispas = config.getTransactionParameters(HKSPA.Id);
56
+ return hispas?.nationalAccountAllowed === false
57
+ ? { iban: account.iban, bic: account.bic }
58
+ : {
59
+ iban: account.iban,
60
+ bic: account.bic,
61
+ accountNumber: account.accountNumber,
62
+ subAccountId: account.subAccountId,
63
+ bank: account.bank,
64
+ };
65
+ }
@@ -11,6 +11,14 @@ export var AccountType;
11
11
  AccountType["InsurancePolicy"] = "InsurancePolicy";
12
12
  AccountType["Miscellaneous"] = "Miscellaneous";
13
13
  })(AccountType || (AccountType = {}));
14
+ /** How an account reference reads in an error message. */
15
+ export function describeAccount(account) {
16
+ if (typeof account === 'string')
17
+ return account;
18
+ return account.subAccountId
19
+ ? `${account.accountNumber} (${account.subAccountId})`
20
+ : account.accountNumber;
21
+ }
14
22
  export function finTsAccountTypeToEnum(accountType) {
15
23
  if (accountType >= 1 && accountType <= 9)
16
24
  return AccountType.CheckingAccount;
package/dist/client.js CHANGED
@@ -1,12 +1,15 @@
1
+ import { describeAccount } from './bankAccount.js';
1
2
  import { FinTSConfig } from './config.js';
2
3
  import { Dialog } from './dialog.js';
3
4
  import { BalanceInteraction, } from './interactions/balanceInteraction.js';
4
5
  import { CreditCardStatementInteraction } from './interactions/creditcardStatementInteraction.js';
6
+ import { ElectronicStatementInteraction, } from './interactions/electronicStatementInteraction.js';
5
7
  import { PortfolioInteraction, } from './interactions/portfolioInteraction.js';
6
8
  import { StatementInteractionCAMT } from './interactions/statementInteractionCAMT.js';
7
9
  import { StatementInteractionMT940 } from './interactions/statementInteractionMT940.js';
8
10
  import { DKKKU } from './segments/DKKKU.js';
9
11
  import { HKCAZ } from './segments/HKCAZ.js';
12
+ import { HKEKA } from './segments/HKEKA.js';
10
13
  import { HKIDN } from './segments/HKIDN.js';
11
14
  import { HKKAZ } from './segments/HKKAZ.js';
12
15
  import { HKSAL } from './segments/HKSAL.js';
@@ -65,21 +68,21 @@ export class FinTSClient {
65
68
  }
66
69
  /**
67
70
  * Checks if the bank supports fetching an account balance in general or for the given account number when provided
68
- * @param accountNumber when the account number is provided, checks if the account supports fetching the balance
71
+ * @param account when the account number is provided, checks if the account supports fetching the balance
69
72
  * @returns true if the bank (and account) supports fetching the account balance
70
73
  */
71
- canGetAccountBalance(accountNumber) {
72
- return accountNumber
73
- ? this.config.isAccountTransactionSupported(accountNumber, HKSAL.Id)
74
+ canGetAccountBalance(account) {
75
+ return account
76
+ ? this.config.isAccountTransactionSupported(account, HKSAL.Id)
74
77
  : this.config.isTransactionSupported(HKSAL.Id);
75
78
  }
76
79
  /**
77
80
  * Fetches the account balance for the given account number
78
- * @param accountNumber - the account number to fetch the balance for, must be an account available in the config.baningInformation.UPD.accounts
81
+ * @param account - the account number to fetch the balance for, must be an account available in the config.baningInformation.UPD.accounts
79
82
  * @returns the account balance response
80
83
  */
81
- async getAccountBalance(accountNumber) {
82
- const response = await this.startCustomerOrderInteraction(new BalanceInteraction(accountNumber));
84
+ async getAccountBalance(account) {
85
+ const response = await this.startCustomerOrderInteraction(new BalanceInteraction(account));
83
86
  return response;
84
87
  }
85
88
  /**
@@ -93,14 +96,14 @@ export class FinTSClient {
93
96
  }
94
97
  /**
95
98
  * Checks if the bank supports fetching account statements in general or for the given account number when provided
96
- * @param accountNumber when the account number is provided, checks if the account supports fetching of statements
99
+ * @param account when the account number is provided, checks if the account supports fetching of statements
97
100
  * @returns true if the bank (and account) supports fetching account statements
98
101
  */
99
- canGetAccountStatements(accountNumber) {
100
- if (accountNumber) {
102
+ canGetAccountStatements(account) {
103
+ if (account) {
101
104
  // Check if either CAMT or MT940 is supported for this account
102
- return (this.config.isAccountTransactionSupported(accountNumber, HKCAZ.Id) ||
103
- this.config.isAccountTransactionSupported(accountNumber, HKKAZ.Id));
105
+ return (this.config.isAccountTransactionSupported(account, HKCAZ.Id) ||
106
+ this.config.isAccountTransactionSupported(account, HKKAZ.Id));
104
107
  }
105
108
  else {
106
109
  // Check if either CAMT or MT940 is supported by the bank
@@ -109,26 +112,26 @@ export class FinTSClient {
109
112
  }
110
113
  /**
111
114
  * Fetches the account statements for the given account number
112
- * @param accountNumber - the account number to fetch the statements for, must be an account available in the config.baningInformation.UPD.accounts
115
+ * @param account - the account number to fetch the statements for, must be an account available in the config.baningInformation.UPD.accounts
113
116
  * @param from - an optional start date of the period to fetch the statements for
114
117
  * @param to - an optional end date of the period to fetch the statements for
115
118
  * @param preferCamt - whether to prefer CAMT format over MT940 when both are supported (default: true)
116
119
  * @returns an account statements response containing an array of statements
117
120
  */
118
- async getAccountStatements(accountNumber, from, to, preferCamt = true) {
121
+ async getAccountStatements(account, from, to, preferCamt = true) {
119
122
  // Check what formats the bank supports
120
- const camtSupported = this.config.isAccountTransactionSupported(accountNumber, 'HKCAZ');
121
- const mt940Supported = this.config.isAccountTransactionSupported(accountNumber, 'HKKAZ');
123
+ const camtSupported = this.config.isAccountTransactionSupported(account, 'HKCAZ');
124
+ const mt940Supported = this.config.isAccountTransactionSupported(account, 'HKKAZ');
122
125
  if (!camtSupported && !mt940Supported) {
123
- throw Error(`Account ${accountNumber} does not support account statements`);
126
+ throw Error(`Account ${describeAccount(account)} does not support account statements`);
124
127
  }
125
128
  // Choose format based on support and preference
126
129
  const useCAMT = (preferCamt && camtSupported) || (!mt940Supported && camtSupported);
127
130
  if (useCAMT) {
128
- return (await this.startCustomerOrderInteraction(new StatementInteractionCAMT(accountNumber, from, to)));
131
+ return (await this.startCustomerOrderInteraction(new StatementInteractionCAMT(account, from, to)));
129
132
  }
130
133
  else {
131
- return (await this.startCustomerOrderInteraction(new StatementInteractionMT940(accountNumber, from, to)));
134
+ return (await this.startCustomerOrderInteraction(new StatementInteractionMT940(account, from, to)));
132
135
  }
133
136
  }
134
137
  /**
@@ -142,24 +145,24 @@ export class FinTSClient {
142
145
  }
143
146
  /**
144
147
  * Checks if the bank supports fetching portfolio information in general or for the given account number when provided
145
- * @param accountNumber when the account number is provided, checks if the account supports fetching of portfolio information
148
+ * @param account when the account number is provided, checks if the account supports fetching of portfolio information
146
149
  * @returns true if the bank (and account) supports fetching portfolio information
147
150
  */
148
- canGetPortfolio(accountNumber) {
149
- return accountNumber
150
- ? this.config.isAccountTransactionSupported(accountNumber, HKWPD.Id)
151
+ canGetPortfolio(account) {
152
+ return account
153
+ ? this.config.isAccountTransactionSupported(account, HKWPD.Id)
151
154
  : this.config.isTransactionSupported(HKWPD.Id);
152
155
  }
153
156
  /**
154
157
  * Fetches the portfolio information for the given depot account number
155
- * @param accountNumber - the depot account number to fetch the portfolio for, must be an account available in the config.bankingInformation.UPD.accounts
158
+ * @param account - the depot account number to fetch the portfolio for, must be an account available in the config.bankingInformation.UPD.accounts
156
159
  * @param currency - optional currency filter for the portfolio statement
157
160
  * @param priceQuality - optional price quality filter ('1' for real-time, '2' for delayed)
158
161
  * @param maxEntries - optional maximum number of entries to retrieve
159
162
  * @returns a portfolio response containing holdings and total value
160
163
  */
161
- async getPortfolio(accountNumber, currency, priceQuality, maxEntries) {
162
- return (await this.startCustomerOrderInteraction(new PortfolioInteraction(accountNumber, currency, priceQuality, maxEntries)));
164
+ async getPortfolio(account, currency, priceQuality, maxEntries) {
165
+ return (await this.startCustomerOrderInteraction(new PortfolioInteraction(account, currency, priceQuality, maxEntries)));
163
166
  }
164
167
  /**
165
168
  * Continues the portfolio fetching when a TAN is required
@@ -173,24 +176,24 @@ export class FinTSClient {
173
176
  }
174
177
  /**
175
178
  * Checks if the bank supports fetching credit card statements in general or for the given account number
176
- * @param accountNumber when the account number is provided, checks if the account supports fetching of statements
179
+ * @param account when the account number is provided, checks if the account supports fetching of statements
177
180
  * @returns true if the bank (and account) supports fetching credit card statements
178
181
  */
179
- canGetCreditCardStatements(accountNumber) {
180
- return accountNumber
181
- ? this.config.isAccountTransactionSupported(accountNumber, DKKKU.Id)
182
+ canGetCreditCardStatements(account) {
183
+ return account
184
+ ? this.config.isAccountTransactionSupported(account, DKKKU.Id)
182
185
  : this.config.isTransactionSupported(DKKKU.Id);
183
186
  }
184
187
  /**
185
188
  * Fetches the credit card statements for the given account number
186
- * @param accountNumber - the account number to fetch the statements for, must be a credit card account available
189
+ * @param account - the account number to fetch the statements for, must be a credit card account available
187
190
  * in the config.baningInformation.UPD.accounts
188
191
  * @param from - an optional start date of the period to fetch the statements for
189
192
  * @param to - an optional end date of the period to fetch the statements for
190
193
  * @returns an account statements response containing an array of statements
191
194
  */
192
- async getCreditCardStatements(accountNumber, from) {
193
- return (await this.startCustomerOrderInteraction(new CreditCardStatementInteraction(accountNumber, from)));
195
+ async getCreditCardStatements(account, from) {
196
+ return (await this.startCustomerOrderInteraction(new CreditCardStatementInteraction(account, from)));
194
197
  }
195
198
  /**
196
199
  * Continues the credit card statements fetching when a TAN is required
@@ -201,6 +204,41 @@ export class FinTSClient {
201
204
  async getCreditCardStatementsWithTan(tanReference, tan) {
202
205
  return (await this.continueCustomerInteractionWithTan([DKKKU.Id], tanReference, tan));
203
206
  }
207
+ /**
208
+ * Checks if the bank supports fetching electronic account statements in general or for the given account number
209
+ * @param account when the account number is provided, checks if the account supports fetching of electronic statements
210
+ * @returns true if the bank (and account) supports fetching electronic account statements
211
+ */
212
+ canGetElectronicStatements(account) {
213
+ return account
214
+ ? this.config.isAccountTransactionSupported(account, HKEKA.Id)
215
+ : this.config.isTransactionSupported(HKEKA.Id);
216
+ }
217
+ /**
218
+ * Fetches an electronic account statement (Elektronischer Kontoauszug) for the given account number
219
+ *
220
+ * This returns the statement document the bank files in the customer's electronic mailbox,
221
+ * usually a PDF, not a list of transactions. The bank hands out one statement per call and
222
+ * announces a waiting successor in `nextOffset`; pass that value back in `options.offset` to
223
+ * fetch the next one. Banks that set `receiptRequired` in their HIEKAS parameters keep
224
+ * offering a statement until it has been acknowledged with its receipt.
225
+ *
226
+ * @param account - the account number to fetch the statement for, must be an account available in the config.bankingInformation.upd.accounts
227
+ * @param options - optional format, statement number and year, entry limit and offset
228
+ * @returns a response containing the statement documents and the offset of a waiting successor
229
+ */
230
+ async getElectronicStatements(account, options) {
231
+ return (await this.startCustomerOrderInteraction(new ElectronicStatementInteraction(account, options)));
232
+ }
233
+ /**
234
+ * Continues the electronic account statement fetching when a TAN is required
235
+ * @param tanReference The TAN reference provided in the first call's response
236
+ * @param tan The TAN entered by the user, can be omitted if a decoupled TAN method is used
237
+ * @returns a response containing the statement documents
238
+ */
239
+ async getElectronicStatementsWithTan(tanReference, tan) {
240
+ return (await this.continueCustomerInteractionWithTan([HKEKA.Id], tanReference, tan));
241
+ }
204
242
  async startCustomerOrderInteraction(interaction) {
205
243
  this.currentDialog = new Dialog(this.config, false);
206
244
  this.currentDialog.addCustomerInteraction(interaction);
package/dist/config.js CHANGED
@@ -157,13 +157,36 @@ export class FinTSConfig {
157
157
  return (this.bankingInformation.bpd?.allowedTransactions.find((t) => t.transId === transId) !==
158
158
  undefined);
159
159
  }
160
+ /**
161
+ * The account the bank meant, without demanding that it be unambiguous.
162
+ *
163
+ * For entries the *bank* supplied — a SEPA account from HISPA, say — rather than
164
+ * ones a caller asked for. A caller who names an ambiguous account has made a
165
+ * mistake worth an exception; a bank listing its own accounts has not, and
166
+ * throwing there would break every dialog at an institution that shares numbers.
167
+ *
168
+ * @param account An account number with, where the bank gave one, its sub-account id
169
+ */
170
+ matchBankAccount(account) {
171
+ const accounts = this.bankingInformation.upd?.bankAccounts ?? [];
172
+ const exactMatch = accounts.find((a) => a.accountNumber === account.accountNumber && a.subAccountId === account.subAccountId);
173
+ if (exactMatch)
174
+ return exactMatch;
175
+ // A tolerance, not a rule: B.3.1 requires the sub-account id to appear the same
176
+ // way in the UPD and in HKSPA/HISPA, and a bank that omits it here has not kept
177
+ // to that. Refusing would cost the IBAN for an account that is otherwise
178
+ // perfectly identified, so a number only one account has still identifies it.
179
+ // One that several share does not, and guessing is what this change exists to stop.
180
+ const matches = accounts.filter((a) => a.accountNumber === account.accountNumber);
181
+ return matches.length === 1 ? matches[0] : undefined;
182
+ }
160
183
  /**
161
184
  * Checks if a transaction is supported for a specific account
162
- * @param accountNumber The account number
185
+ * @param account An account number, or an account from `bankingInformation.upd.bankAccounts`
163
186
  * @param transId The transaction ID
164
187
  */
165
- isAccountTransactionSupported(accountNumber, transId) {
166
- const bankAccount = this.getBankAccount(accountNumber);
188
+ isAccountTransactionSupported(account, transId) {
189
+ const bankAccount = this.getBankAccount(account);
167
190
  return !!bankAccount.allowedTransactions?.find((t) => t.transId === transId);
168
191
  }
169
192
  /**
@@ -181,14 +204,38 @@ export class FinTSConfig {
181
204
  return maxSupportedversion;
182
205
  }
183
206
  /**
184
- * Gets the bank account information for a specific account number
185
- * @param accountNumber The account number
207
+ * Resolves an account reference against the accounts the bank reported.
208
+ *
209
+ * A number alone is enough wherever it is unique, which is the usual case. Where
210
+ * it is not, this throws instead of picking one: FinTS identifies an account by
211
+ * number *and* sub-account id, so a number that matches two accounts does not say
212
+ * which one is meant, and answering for the wrong one produces a balance or a list
213
+ * of transactions that belongs to a different account with nothing to indicate it.
214
+ *
215
+ * @param account An account number, or an account from `bankingInformation.upd.bankAccounts`
186
216
  */
187
- getBankAccount(accountNumber) {
188
- const bankAccount = this.bankingInformation.upd?.bankAccounts.find((a) => a.accountNumber === accountNumber);
189
- if (!bankAccount) {
190
- throw Error(`Account ${accountNumber} not found in UPD`);
217
+ getBankAccount(account) {
218
+ const accounts = this.bankingInformation.upd?.bankAccounts ?? [];
219
+ if (typeof account !== 'string') {
220
+ // Resolved against the UPD rather than trusted as given: the caller may hold
221
+ // an account from an earlier session, and the entry the bank sent this time
222
+ // is the one carrying the current allowed transactions.
223
+ const matchedAccount = accounts.find((a) => a.accountNumber === account.accountNumber && a.subAccountId === account.subAccountId);
224
+ if (!matchedAccount) {
225
+ throw Error(`Account ${account.accountNumber}${account.subAccountId ? ` (${account.subAccountId})` : ''} not found in UPD`);
226
+ }
227
+ return matchedAccount;
228
+ }
229
+ const matches = accounts.filter((a) => a.accountNumber === account);
230
+ if (matches.length === 0) {
231
+ throw Error(`Account ${account} not found in UPD`);
232
+ }
233
+ if (matches.length > 1) {
234
+ const subAccountIds = matches.map((a) => a.subAccountId ?? '(none)').join(', ');
235
+ throw Error(`Account number ${account} is not unique in UPD: ${matches.length} accounts share it, ` +
236
+ `with sub-account ids ${subAccountIds}. Pass the account itself instead of its number, ` +
237
+ `from bankingInformation.upd.bankAccounts.`);
191
238
  }
192
- return bankAccount;
239
+ return matches[0];
193
240
  }
194
241
  }
@@ -14,7 +14,24 @@ export class Binary extends DataElement {
14
14
  }
15
15
  return `@${value.length}@${value}`;
16
16
  }
17
+ /**
18
+ * A binary value arrives as `@<length>@<data>`. The length is authoritative: it is the
19
+ * only thing that tells data apart from the separators and escape characters that a
20
+ * binary payload — a PDF, for instance — is full of. Returning everything after the
21
+ * second `@` instead would hand out whatever the bank appended between the end of the
22
+ * data and the next separator.
23
+ */
17
24
  decode(text) {
18
- return text.slice(text.indexOf('@', 1) + 1);
25
+ if (text[0] !== '@') {
26
+ // Not length-prefixed — nothing to go by, take it as it is.
27
+ return text;
28
+ }
29
+ const lengthEnd = text.indexOf('@', 1);
30
+ if (lengthEnd < 0) {
31
+ return text;
32
+ }
33
+ const dataStart = lengthEnd + 1;
34
+ const length = Number.parseInt(text.slice(1, lengthEnd), 10);
35
+ return Number.isNaN(length) ? text.slice(dataStart) : text.slice(dataStart, dataStart + length);
19
36
  }
20
37
  }
package/dist/dialog.js CHANGED
@@ -180,39 +180,60 @@ export class Dialog {
180
180
  }
181
181
  return this.config.tanMediaName;
182
182
  }
183
+ /**
184
+ * Collects a response that the bank spreads over several messages.
185
+ *
186
+ * When the bank cannot fit a response into one message it answers with code 3040 plus
187
+ * a continuation mark. Repeating the order with that mark yields the next portion —
188
+ * as a COMPLETE, self-contained response segment, not as a byte-wise continuation of
189
+ * the previous one. A HICAZ follow-up, for example, repeats the account and the CAMT
190
+ * descriptor before carrying its own share of the statements.
191
+ *
192
+ * Every portion is therefore decoded on its own and all of them are placed into the
193
+ * response message the caller holds. Combining their payloads needs to know what the
194
+ * payload means — one MT940 stream continues, a list of CAMT documents is appended —
195
+ * so that step belongs to the interaction, which does it via `findAllSegments`.
196
+ */
183
197
  async handlePartedMessages(message, responseMessage, interaction) {
184
- let partedSegment = responseMessage.findSegment(PARTED.Id);
185
- if (partedSegment) {
186
- while (responseMessage.hasReturnCode(3040)) {
187
- const answers = responseMessage.getBankAnswers();
188
- const segmentWithContinuation = message.segments.find((s) => s.header.segId === interaction.segId);
189
- if (!segmentWithContinuation) {
190
- throw new Error(`Response contains segment with further information, but corresponding segment could not be found or is not specified`);
191
- }
192
- const answer = answers.find((a) => a.code === 3040);
193
- if (!answer || !answer.params || answer.params.length === 0) {
194
- throw new Error('Expected bank answer to contain continuation mark parameters (code 3040)');
195
- }
196
- segmentWithContinuation.continuationMark = answer.params[0];
197
- const hnhbkSegment = message.findSegment(HNHBK.Id);
198
- if (!hnhbkSegment) {
199
- throw new Error('HNHBK segment not found in message');
200
- }
201
- hnhbkSegment.msgNr = ++this.lastMessageNumber;
202
- const nextResponseMessage = await this.httpClient.sendMessage(message);
203
- const nextPartedSegment = nextResponseMessage.findSegment(PARTED.Id);
204
- if (nextPartedSegment) {
205
- nextPartedSegment.rawData =
206
- partedSegment.rawData +
207
- nextPartedSegment.rawData.slice(nextPartedSegment.rawData.indexOf('+') + 1);
208
- partedSegment = nextPartedSegment;
209
- }
210
- responseMessage = nextResponseMessage;
211
- }
212
- const completeSegment = decode(partedSegment.rawData);
213
- const index = responseMessage.segments.indexOf(partedSegment);
214
- responseMessage.segments.splice(index, 1, completeSegment);
198
+ // ALL of them, not just the first: one bank message may well carry several
199
+ // response segments. Taking only the first left the rest sitting in the tree as
200
+ // PARTED, where `findAllSegments` cannot see them — lost without a trace.
201
+ const partedSegments = responseMessage.findAllSegments(PARTED.Id);
202
+ if (partedSegments.length === 0) {
203
+ return;
215
204
  }
205
+ // The message the caller holds — every portion has to end up in THIS one, not in
206
+ // the last one we happen to receive.
207
+ const callersMessage = responseMessage;
208
+ const rawPortions = partedSegments.map((segment) => segment.rawData);
209
+ while (responseMessage.hasReturnCode(3040)) {
210
+ const answers = responseMessage.getBankAnswers();
211
+ const segmentWithContinuation = message.segments.find((s) => s.header.segId === interaction.segId);
212
+ if (!segmentWithContinuation) {
213
+ throw new Error(`Response contains segment with further information, but corresponding segment could not be found or is not specified`);
214
+ }
215
+ const answer = answers.find((a) => a.code === 3040);
216
+ if (!answer || !answer.params || answer.params.length === 0) {
217
+ throw new Error('Expected bank answer to contain continuation mark parameters (code 3040)');
218
+ }
219
+ segmentWithContinuation.continuationMark = answer.params[0];
220
+ const hnhbkSegment = message.findSegment(HNHBK.Id);
221
+ if (!hnhbkSegment) {
222
+ throw new Error('HNHBK segment not found in message');
223
+ }
224
+ hnhbkSegment.msgNr = ++this.lastMessageNumber;
225
+ const nextResponseMessage = await this.httpClient.sendMessage(message);
226
+ rawPortions.push(...nextResponseMessage
227
+ .findAllSegments(PARTED.Id)
228
+ .map((segment) => segment.rawData));
229
+ responseMessage = nextResponseMessage;
230
+ }
231
+ // Every PARTED placeholder gives way to the decoded portions, at the position of
232
+ // the first one so the segment order stays intact.
233
+ const index = callersMessage.segments.indexOf(partedSegments[0]);
234
+ const withoutPlaceholders = callersMessage.segments.filter((segment) => segment.header.segId !== PARTED.Id);
235
+ withoutPlaceholders.splice(index, 0, ...rawPortions.map((raw) => decode(raw)));
236
+ callersMessage.segments = withoutPlaceholders;
216
237
  }
217
238
  checkEnded(response) {
218
239
  if (response.bankAnswers.some((answer) => answer.code === 100) ||
@@ -0,0 +1 @@
1
+ export {};
@@ -10,7 +10,7 @@ export class HttpClient {
10
10
  }
11
11
  async sendMessage(message) {
12
12
  const encodedMessage = message.encode();
13
- const requestBuffer = Buffer.from(encodedMessage);
13
+ const requestBuffer = Buffer.from(encodedMessage, 'latin1');
14
14
  if (this.debug) {
15
15
  console.log('Request Message:\n');
16
16
  if (this.debugRaw) {
package/dist/index.js CHANGED
@@ -8,10 +8,12 @@ export * from './bpd.js';
8
8
  export * from './client.js';
9
9
  export * from './config.js';
10
10
  export * from './dialog.js';
11
+ export * from './electronicStatement.js';
11
12
  export * from './httpClient.js';
12
13
  export * from './message.js';
13
14
  export * from './mt535parser.js';
14
15
  export * from './mt940parser.js';
15
16
  export * from './segment.js';
17
+ export { StatementFormat } from './segments/HKEKA.js';
16
18
  export * from './statement.js';
17
19
  export * from './upd.js';