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
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,11 +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 | ✓ | ✓ |
204
- | **Electronic Statements** | `getElectronicStatements(accountNumber, options?)` | Fetches the statement document from the electronic mailbox, usually a PDF | HKEKA | ✓ | ✓ |
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 | ✓ | ✓ |
205
208
  | **TAN Method Selection** | `selectTanMethod(tanMethodId)` | Selects a TAN method by ID from available methods | - | ❌ | ❌ |
206
209
  | **TAN Media Selection** | `selectTanMedia(tanMediaName)` | Selects a specific TAN media device by name | - | ❌ | ❌ |
207
210
 
@@ -211,11 +214,11 @@ For each account-specific transaction, the client provides corresponding `can*`
211
214
 
212
215
  | Support Check Method | Purpose |
213
216
  | -------------------------------------------- | --------------------------------------------------------------- |
214
- | `canGetAccountBalance(accountNumber?)` | Checks if account balance fetching is supported |
215
- | `canGetAccountStatements(accountNumber?)` | Checks if account statements fetching is supported (MT940/CAMT) |
216
- | `canGetPortfolio(accountNumber?)` | Checks if portfolio information fetching is supported |
217
- | `canGetCreditCardStatements(accountNumber?)` | Checks if credit card statements fetching is supported |
218
- | `canGetElectronicStatements(accountNumber?)` | Checks if electronic account 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 |
219
222
 
220
223
  ### Transaction Parameters
221
224
 
@@ -246,12 +249,12 @@ if (config.isTransactionSupported('HKWPD')) {
246
249
  }
247
250
  ```
248
251
 
249
- #### `config.isAccountTransactionSupported(accountNumber: string, transId: string): boolean`
252
+ #### `config.isAccountTransactionSupported(account: AccountRef, transId: string): boolean`
250
253
 
251
254
  Checks whether a specific transaction type is supported for a particular account.
252
255
 
253
256
  ```typescript
254
- if (config.isAccountTransactionSupported('1234567890', 'HKWPD')) {
257
+ if (config.isAccountTransactionSupported(account, 'HKWPD')) {
255
258
  console.log('Account supports portfolio requests');
256
259
  }
257
260
  ```
@@ -280,6 +283,7 @@ Implementing further transactions should be straight forward and contributions a
280
283
 
281
284
  ### Successfully tested with the following banks
282
285
 
286
+ - comdirect
283
287
  - DKB
284
288
  - ING-DiBa
285
289
  - Renault Bank Direkt
@@ -301,7 +305,7 @@ As this is a free-time project, a lot of things are still remaining which could
301
305
 
302
306
  ## License
303
307
 
304
- 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
305
309
 
306
310
  ## References
307
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,3 +1,4 @@
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';
@@ -67,21 +68,21 @@ export class FinTSClient {
67
68
  }
68
69
  /**
69
70
  * Checks if the bank supports fetching an account balance in general or for the given account number when provided
70
- * @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
71
72
  * @returns true if the bank (and account) supports fetching the account balance
72
73
  */
73
- canGetAccountBalance(accountNumber) {
74
- return accountNumber
75
- ? this.config.isAccountTransactionSupported(accountNumber, HKSAL.Id)
74
+ canGetAccountBalance(account) {
75
+ return account
76
+ ? this.config.isAccountTransactionSupported(account, HKSAL.Id)
76
77
  : this.config.isTransactionSupported(HKSAL.Id);
77
78
  }
78
79
  /**
79
80
  * Fetches the account balance for the given account number
80
- * @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
81
82
  * @returns the account balance response
82
83
  */
83
- async getAccountBalance(accountNumber) {
84
- const response = await this.startCustomerOrderInteraction(new BalanceInteraction(accountNumber));
84
+ async getAccountBalance(account) {
85
+ const response = await this.startCustomerOrderInteraction(new BalanceInteraction(account));
85
86
  return response;
86
87
  }
87
88
  /**
@@ -95,14 +96,14 @@ export class FinTSClient {
95
96
  }
96
97
  /**
97
98
  * Checks if the bank supports fetching account statements in general or for the given account number when provided
98
- * @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
99
100
  * @returns true if the bank (and account) supports fetching account statements
100
101
  */
101
- canGetAccountStatements(accountNumber) {
102
- if (accountNumber) {
102
+ canGetAccountStatements(account) {
103
+ if (account) {
103
104
  // Check if either CAMT or MT940 is supported for this account
104
- return (this.config.isAccountTransactionSupported(accountNumber, HKCAZ.Id) ||
105
- this.config.isAccountTransactionSupported(accountNumber, HKKAZ.Id));
105
+ return (this.config.isAccountTransactionSupported(account, HKCAZ.Id) ||
106
+ this.config.isAccountTransactionSupported(account, HKKAZ.Id));
106
107
  }
107
108
  else {
108
109
  // Check if either CAMT or MT940 is supported by the bank
@@ -111,26 +112,26 @@ export class FinTSClient {
111
112
  }
112
113
  /**
113
114
  * Fetches the account statements for the given account number
114
- * @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
115
116
  * @param from - an optional start date of the period to fetch the statements for
116
117
  * @param to - an optional end date of the period to fetch the statements for
117
118
  * @param preferCamt - whether to prefer CAMT format over MT940 when both are supported (default: true)
118
119
  * @returns an account statements response containing an array of statements
119
120
  */
120
- async getAccountStatements(accountNumber, from, to, preferCamt = true) {
121
+ async getAccountStatements(account, from, to, preferCamt = true) {
121
122
  // Check what formats the bank supports
122
- const camtSupported = this.config.isAccountTransactionSupported(accountNumber, 'HKCAZ');
123
- const mt940Supported = this.config.isAccountTransactionSupported(accountNumber, 'HKKAZ');
123
+ const camtSupported = this.config.isAccountTransactionSupported(account, 'HKCAZ');
124
+ const mt940Supported = this.config.isAccountTransactionSupported(account, 'HKKAZ');
124
125
  if (!camtSupported && !mt940Supported) {
125
- throw Error(`Account ${accountNumber} does not support account statements`);
126
+ throw Error(`Account ${describeAccount(account)} does not support account statements`);
126
127
  }
127
128
  // Choose format based on support and preference
128
129
  const useCAMT = (preferCamt && camtSupported) || (!mt940Supported && camtSupported);
129
130
  if (useCAMT) {
130
- return (await this.startCustomerOrderInteraction(new StatementInteractionCAMT(accountNumber, from, to)));
131
+ return (await this.startCustomerOrderInteraction(new StatementInteractionCAMT(account, from, to)));
131
132
  }
132
133
  else {
133
- return (await this.startCustomerOrderInteraction(new StatementInteractionMT940(accountNumber, from, to)));
134
+ return (await this.startCustomerOrderInteraction(new StatementInteractionMT940(account, from, to)));
134
135
  }
135
136
  }
136
137
  /**
@@ -144,24 +145,24 @@ export class FinTSClient {
144
145
  }
145
146
  /**
146
147
  * Checks if the bank supports fetching portfolio information in general or for the given account number when provided
147
- * @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
148
149
  * @returns true if the bank (and account) supports fetching portfolio information
149
150
  */
150
- canGetPortfolio(accountNumber) {
151
- return accountNumber
152
- ? this.config.isAccountTransactionSupported(accountNumber, HKWPD.Id)
151
+ canGetPortfolio(account) {
152
+ return account
153
+ ? this.config.isAccountTransactionSupported(account, HKWPD.Id)
153
154
  : this.config.isTransactionSupported(HKWPD.Id);
154
155
  }
155
156
  /**
156
157
  * Fetches the portfolio information for the given depot account number
157
- * @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
158
159
  * @param currency - optional currency filter for the portfolio statement
159
160
  * @param priceQuality - optional price quality filter ('1' for real-time, '2' for delayed)
160
161
  * @param maxEntries - optional maximum number of entries to retrieve
161
162
  * @returns a portfolio response containing holdings and total value
162
163
  */
163
- async getPortfolio(accountNumber, currency, priceQuality, maxEntries) {
164
- 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)));
165
166
  }
166
167
  /**
167
168
  * Continues the portfolio fetching when a TAN is required
@@ -175,24 +176,24 @@ export class FinTSClient {
175
176
  }
176
177
  /**
177
178
  * Checks if the bank supports fetching credit card statements in general or for the given account number
178
- * @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
179
180
  * @returns true if the bank (and account) supports fetching credit card statements
180
181
  */
181
- canGetCreditCardStatements(accountNumber) {
182
- return accountNumber
183
- ? this.config.isAccountTransactionSupported(accountNumber, DKKKU.Id)
182
+ canGetCreditCardStatements(account) {
183
+ return account
184
+ ? this.config.isAccountTransactionSupported(account, DKKKU.Id)
184
185
  : this.config.isTransactionSupported(DKKKU.Id);
185
186
  }
186
187
  /**
187
188
  * Fetches the credit card statements for the given account number
188
- * @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
189
190
  * in the config.baningInformation.UPD.accounts
190
191
  * @param from - an optional start date of the period to fetch the statements for
191
192
  * @param to - an optional end date of the period to fetch the statements for
192
193
  * @returns an account statements response containing an array of statements
193
194
  */
194
- async getCreditCardStatements(accountNumber, from) {
195
- return (await this.startCustomerOrderInteraction(new CreditCardStatementInteraction(accountNumber, from)));
195
+ async getCreditCardStatements(account, from) {
196
+ return (await this.startCustomerOrderInteraction(new CreditCardStatementInteraction(account, from)));
196
197
  }
197
198
  /**
198
199
  * Continues the credit card statements fetching when a TAN is required
@@ -205,12 +206,12 @@ export class FinTSClient {
205
206
  }
206
207
  /**
207
208
  * Checks if the bank supports fetching electronic account statements in general or for the given account number
208
- * @param accountNumber when the account number is provided, checks if the account supports fetching of electronic statements
209
+ * @param account when the account number is provided, checks if the account supports fetching of electronic statements
209
210
  * @returns true if the bank (and account) supports fetching electronic account statements
210
211
  */
211
- canGetElectronicStatements(accountNumber) {
212
- return accountNumber
213
- ? this.config.isAccountTransactionSupported(accountNumber, HKEKA.Id)
212
+ canGetElectronicStatements(account) {
213
+ return account
214
+ ? this.config.isAccountTransactionSupported(account, HKEKA.Id)
214
215
  : this.config.isTransactionSupported(HKEKA.Id);
215
216
  }
216
217
  /**
@@ -222,12 +223,12 @@ export class FinTSClient {
222
223
  * fetch the next one. Banks that set `receiptRequired` in their HIEKAS parameters keep
223
224
  * offering a statement until it has been acknowledged with its receipt.
224
225
  *
225
- * @param accountNumber - the account number to fetch the statement for, must be an account available in the config.bankingInformation.upd.accounts
226
+ * @param account - the account number to fetch the statement for, must be an account available in the config.bankingInformation.upd.accounts
226
227
  * @param options - optional format, statement number and year, entry limit and offset
227
228
  * @returns a response containing the statement documents and the offset of a waiting successor
228
229
  */
229
- async getElectronicStatements(accountNumber, options) {
230
- return (await this.startCustomerOrderInteraction(new ElectronicStatementInteraction(accountNumber, options)));
230
+ async getElectronicStatements(account, options) {
231
+ return (await this.startCustomerOrderInteraction(new ElectronicStatementInteraction(account, options)));
231
232
  }
232
233
  /**
233
234
  * Continues the electronic account statement fetching when a TAN is required
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
  }
@@ -1,26 +1,28 @@
1
+ import { internationalAccount, nationalAccount } from '../accountDescriptor.js';
2
+ import { describeAccount } from '../bankAccount.js';
1
3
  import { CreditDebit } from '../codes.js';
2
4
  import { HISAL } from '../segments/HISAL.js';
3
5
  import { HKSAL } from '../segments/HKSAL.js';
4
6
  import { CustomerOrderInteraction } from './customerInteraction.js';
5
7
  export class BalanceInteraction extends CustomerOrderInteraction {
6
- accountNumber;
7
- constructor(accountNumber) {
8
+ account;
9
+ constructor(account) {
8
10
  super(HKSAL.Id, HISAL.Id);
9
- this.accountNumber = accountNumber;
11
+ this.account = account;
10
12
  }
11
13
  createSegments(init) {
12
- const bankAccount = init.getBankAccount(this.accountNumber);
13
- if (!init.isAccountTransactionSupported(this.accountNumber, this.segId)) {
14
- throw Error(`Account ${this.accountNumber} does not support business transaction '${this.segId}'`);
14
+ const bankAccount = init.getBankAccount(this.account);
15
+ if (!init.isAccountTransactionSupported(this.account, this.segId)) {
16
+ throw Error(`Account ${describeAccount(this.account)} does not support business transaction '${this.segId}'`);
15
17
  }
16
18
  const version = init.getMaxSupportedTransactionVersion(HKSAL.Id);
17
19
  if (!version) {
18
20
  throw Error(`There is no supported version for business transaction '${HKSAL.Id}`);
19
21
  }
20
- const account = version <= 6 ? { ...bankAccount, iban: undefined, bic: undefined } : bankAccount;
22
+ const account = version <= 6 ? nationalAccount(bankAccount) : internationalAccount(init, bankAccount);
21
23
  const hksal = {
22
24
  header: { segId: HKSAL.Id, segNr: 0, version: version },
23
- account,
25
+ account: account,
24
26
  allAccounts: false,
25
27
  };
26
28
  return [hksal];
@@ -1,18 +1,19 @@
1
+ import { describeAccount } from '../bankAccount.js';
1
2
  import { DIKKU } from '../segments/DIKKU.js';
2
3
  import { DKKKU } from '../segments/DKKKU.js';
3
4
  import { CustomerOrderInteraction } from './customerInteraction.js';
4
5
  export class CreditCardStatementInteraction extends CustomerOrderInteraction {
5
- accountNumber;
6
+ account;
6
7
  from;
7
- constructor(accountNumber, from) {
8
+ constructor(account, from) {
8
9
  super(DKKKU.Id, DIKKU.Id);
9
- this.accountNumber = accountNumber;
10
+ this.account = account;
10
11
  this.from = from;
11
12
  }
12
13
  createSegments(init) {
13
- const bankAccount = init.getBankAccount(this.accountNumber);
14
- if (!init.isAccountTransactionSupported(this.accountNumber, this.segId)) {
15
- throw Error(`Account ${this.accountNumber} does not support business transaction '${this.segId}'`);
14
+ const bankAccount = init.getBankAccount(this.account);
15
+ if (!init.isAccountTransactionSupported(this.account, this.segId)) {
16
+ throw Error(`Account ${describeAccount(this.account)} does not support business transaction '${this.segId}'`);
16
17
  }
17
18
  const account = { ...bankAccount, iban: undefined };
18
19
  const version = 2;
@@ -58,7 +59,7 @@ export class CreditCardStatementInteraction extends CustomerOrderInteraction {
58
59
  if (dikku.transactions) {
59
60
  for (let i = 0; i < dikku.transactions.length; i++) {
60
61
  const parts = dikku.transactions[i].split(':');
61
- // const accountNumber = parts[0];
62
+ // const account = parts[0];
62
63
  const transactionDateStr = parts[1];
63
64
  const valueDateStr = parts[2];
64
65
  const currencyOrig = parts[5];
@@ -1,3 +1,4 @@
1
+ import { internationalAccount, nationalAccount } from '../accountDescriptor.js';
1
2
  import { HIEKA } from '../segments/HIEKA.js';
2
3
  import { HKEKA } from '../segments/HKEKA.js';
3
4
  import { CustomerOrderInteraction } from './customerInteraction.js';
@@ -36,15 +37,15 @@ function unwrapBase64(bytes) {
36
37
  }
37
38
  }
38
39
  export class ElectronicStatementInteraction extends CustomerOrderInteraction {
39
- accountNumber;
40
+ account;
40
41
  options;
41
- constructor(accountNumber, options = {}) {
42
+ constructor(account, options = {}) {
42
43
  super(HKEKA.Id, HIEKA.Id);
43
- this.accountNumber = accountNumber;
44
+ this.account = account;
44
45
  this.options = options;
45
46
  }
46
47
  createSegments(init) {
47
- const bankAccount = init.getBankAccount(this.accountNumber);
48
+ const bankAccount = init.getBankAccount(this.account);
48
49
  const version = init.getMaxSupportedTransactionVersion(HKEKA.Id);
49
50
  if (!version) {
50
51
  throw Error(`There is no supported version for business transaction '${HKEKA.Id}'`);
@@ -53,7 +54,7 @@ export class ElectronicStatementInteraction extends CustomerOrderInteraction {
53
54
  const format = this.options.format ?? params?.supportedFormats?.[0];
54
55
  const hkeka = {
55
56
  header: { segId: HKEKA.Id, segNr: 0, version: version },
56
- account: bankAccount,
57
+ account: version <= 3 ? nationalAccount(bankAccount) : internationalAccount(init, bankAccount),
57
58
  statementFormat: format,
58
59
  statementNumber: this.options.number,
59
60
  statementYear: this.options.year,
@@ -1,3 +1,5 @@
1
+ import { nationalAccount } from '../accountDescriptor.js';
2
+ import { describeAccount } from '../bankAccount.js';
1
3
  import { Mt535Parser } from '../mt535parser.js';
2
4
  import { HIWPD } from '../segments/HIWPD.js';
3
5
  import { HKWPD } from '../segments/HKWPD.js';
@@ -6,25 +8,25 @@ import { CustomerOrderInteraction } from './customerInteraction.js';
6
8
  * Interaction for requesting and parsing stock portfolio information (HKWPD/HIWPD)
7
9
  */
8
10
  export class PortfolioInteraction extends CustomerOrderInteraction {
9
- accountNumber;
11
+ account;
10
12
  currency;
11
13
  priceQuality;
12
14
  maxEntries;
13
15
  paginationMarker;
14
- constructor(accountNumber, currency, priceQuality, maxEntries, paginationMarker) {
16
+ constructor(account, currency, priceQuality, maxEntries, paginationMarker) {
15
17
  super(HKWPD.Id, HIWPD.Id);
16
- this.accountNumber = accountNumber;
18
+ this.account = account;
17
19
  this.currency = currency;
18
20
  this.priceQuality = priceQuality;
19
21
  this.maxEntries = maxEntries;
20
22
  this.paginationMarker = paginationMarker;
21
23
  }
22
24
  createSegments(config) {
23
- const bankAccount = config.getBankAccount(this.accountNumber);
24
- if (!config.isAccountTransactionSupported(this.accountNumber, this.segId)) {
25
- throw Error(`Account ${this.accountNumber} does not support business transaction '${this.segId}'`);
25
+ const bankAccount = config.getBankAccount(this.account);
26
+ if (!config.isAccountTransactionSupported(this.account, this.segId)) {
27
+ throw Error(`Account ${describeAccount(this.account)} does not support business transaction '${this.segId}'`);
26
28
  }
27
- const depotAccount = { ...bankAccount, iban: undefined }; // HKWPD uses KTV which doesn't have IBAN
29
+ const depotAccount = nationalAccount(bankAccount); // HKWPD uses KTV, which has no IBAN
28
30
  const version = config.getMaxSupportedTransactionVersion(HKWPD.Id);
29
31
  if (!version) {
30
32
  throw Error(`There is no supported version for business transaction '${HKWPD.Id}'`);
@@ -4,7 +4,7 @@ import { CustomerOrderInteraction } from './customerInteraction.js';
4
4
  export class SepaAccountInteraction extends CustomerOrderInteraction {
5
5
  accounts;
6
6
  maxEntries;
7
- constructor(accounts, // optional specific account numbers
7
+ constructor(accounts, // optional: only these accounts
8
8
  maxEntries) {
9
9
  super(HKSPA.Id, HISPA.Id);
10
10
  this.accounts = accounts;
@@ -18,9 +18,7 @@ export class SepaAccountInteraction extends CustomerOrderInteraction {
18
18
  if (!version) {
19
19
  throw Error(`There is no supported version for business transaction '${HKSPA.Id}'`);
20
20
  }
21
- const accounts = this.accounts?.map((accountNumber) => {
22
- return init.getBankAccount(accountNumber);
23
- });
21
+ const accounts = this.accounts?.map((account) => init.getBankAccount(account));
24
22
  const hkspa = {
25
23
  header: { segId: HKSPA.Id, segNr: 0, version: version },
26
24
  accounts: accounts,
@@ -38,7 +36,10 @@ export class SepaAccountInteraction extends CustomerOrderInteraction {
38
36
  bankAccount.isSepaAccount = false;
39
37
  });
40
38
  clientResponse.sepaAccounts.forEach((sepaAccount) => {
41
- const bankAccount = this.dialog?.config.getBankAccount(sepaAccount.accountNumber);
39
+ // Matched, not resolved: this is the bank listing its own accounts, and at an
40
+ // institution where two of them share a number, demanding an unambiguous
41
+ // answer here would fail every dialog before it reached its order.
42
+ const bankAccount = this.dialog?.config.matchBankAccount(sepaAccount);
42
43
  if (bankAccount && !bankAccount.isSepaAccount) {
43
44
  bankAccount.isSepaAccount = sepaAccount.isSepaAccount;
44
45
  bankAccount.iban = sepaAccount.iban;