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.
- package/README.md +21 -15
- package/dist/accountDescriptor.js +65 -0
- package/dist/bankAccount.js +8 -0
- package/dist/client.js +71 -33
- package/dist/config.js +57 -10
- package/dist/dataElements/Binary.js +18 -1
- package/dist/dialog.js +52 -31
- package/dist/electronicStatement.js +1 -0
- package/dist/httpClient.js +1 -1
- package/dist/index.js +2 -0
- package/dist/interactions/balanceInteraction.js +10 -8
- package/dist/interactions/creditcardStatementInteraction.js +17 -8
- package/dist/interactions/electronicStatementInteraction.js +89 -0
- package/dist/interactions/portfolioInteraction.js +19 -11
- package/dist/interactions/sepaAccountInteraction.js +11 -8
- package/dist/interactions/statementInteractionCAMT.js +14 -8
- package/dist/interactions/statementInteractionMT940.js +17 -9
- package/dist/message.js +10 -2
- package/dist/segments/HIEKA.js +45 -0
- package/dist/segments/HIEKAS.js +18 -0
- package/dist/segments/HKEKA.js +43 -0
- package/dist/segments/HKKAZ.js +1 -1
- package/dist/segments/registry.js +6 -0
- package/dist/tests/HKEKA.test.js +198 -0
- package/dist/tests/accountDescriptor.test.js +99 -0
- package/dist/tests/accountReference.test.js +126 -0
- package/dist/tests/interactionAccountDescriptor.test.js +153 -0
- package/dist/tests/message.test.js +18 -0
- package/dist/tests/partedResponse.test.js +181 -0
- package/dist/types/accountDescriptor.d.ts +49 -0
- package/dist/types/accountDescriptor.d.ts.map +1 -0
- package/dist/types/bankAccount.d.ts +13 -0
- package/dist/types/bankAccount.d.ts.map +1 -1
- package/dist/types/client.d.ts +45 -16
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/config.d.ts +27 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/dataElements/Binary.d.ts +7 -0
- package/dist/types/dataElements/Binary.d.ts.map +1 -1
- package/dist/types/dialog.d.ts +14 -0
- package/dist/types/dialog.d.ts.map +1 -1
- package/dist/types/electronicStatement.d.ts +40 -0
- package/dist/types/electronicStatement.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/interactions/balanceInteraction.d.ts +3 -2
- package/dist/types/interactions/balanceInteraction.d.ts.map +1 -1
- package/dist/types/interactions/creditcardStatementInteraction.d.ts +3 -2
- package/dist/types/interactions/creditcardStatementInteraction.d.ts.map +1 -1
- package/dist/types/interactions/electronicStatementInteraction.d.ts +34 -0
- package/dist/types/interactions/electronicStatementInteraction.d.ts.map +1 -0
- package/dist/types/interactions/portfolioInteraction.d.ts +3 -2
- package/dist/types/interactions/portfolioInteraction.d.ts.map +1 -1
- package/dist/types/interactions/sepaAccountInteraction.d.ts +3 -2
- package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -1
- package/dist/types/interactions/statementInteractionCAMT.d.ts +3 -2
- package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -1
- package/dist/types/interactions/statementInteractionMT940.d.ts +3 -2
- package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/segments/HIEKA.d.ts +52 -0
- package/dist/types/segments/HIEKA.d.ts.map +1 -0
- package/dist/types/segments/HIEKAS.d.ts +21 -0
- package/dist/types/segments/HIEKAS.d.ts.map +1 -0
- package/dist/types/segments/HKEKA.d.ts +54 -0
- package/dist/types/segments/HKEKA.d.ts.map +1 -0
- package/dist/types/segments/HKKAZ.d.ts +2 -2
- package/dist/types/segments/HKKAZ.d.ts.map +1 -1
- package/dist/types/segments/registry.d.ts.map +1 -1
- package/dist/types/tests/HKEKA.test.d.ts +2 -0
- package/dist/types/tests/HKEKA.test.d.ts.map +1 -0
- package/dist/types/tests/accountDescriptor.test.d.ts +2 -0
- package/dist/types/tests/accountDescriptor.test.d.ts.map +1 -0
- package/dist/types/tests/accountReference.test.d.ts +2 -0
- package/dist/types/tests/accountReference.test.d.ts.map +1 -0
- package/dist/types/tests/interactionAccountDescriptor.test.d.ts +2 -0
- package/dist/types/tests/interactionAccountDescriptor.test.d.ts.map +1 -0
- package/dist/types/tests/partedResponse.test.d.ts +2 -0
- package/dist/types/tests/partedResponse.test.d.ts.map +1 -0
- 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
|
|
92
|
+
const balanceResponse = await client.getAccountBalance(account);
|
|
90
93
|
|
|
91
94
|
// fetch all available statements
|
|
92
|
-
const statementResponse = await client.getAccountStatements(account
|
|
95
|
+
const statementResponse = await client.getAccountStatements(account);
|
|
93
96
|
|
|
94
97
|
// or fetch portfolio from a securities account
|
|
95
|
-
client.getPortfolio(account
|
|
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
|
|
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(
|
|
201
|
-
| **Account Statements** | `getAccountStatements(
|
|
202
|
-
| **Portfolio** | `getPortfolio(
|
|
203
|
-
| **Credit Card Statements** | `getCreditCardStatements(
|
|
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(
|
|
214
|
-
| `canGetAccountStatements(
|
|
215
|
-
| `canGetPortfolio(
|
|
216
|
-
| `canGetCreditCardStatements(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
+
}
|
package/dist/bankAccount.js
CHANGED
|
@@ -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
|
|
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(
|
|
72
|
-
return
|
|
73
|
-
? this.config.isAccountTransactionSupported(
|
|
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
|
|
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(
|
|
82
|
-
const response = await this.startCustomerOrderInteraction(new BalanceInteraction(
|
|
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
|
|
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(
|
|
100
|
-
if (
|
|
102
|
+
canGetAccountStatements(account) {
|
|
103
|
+
if (account) {
|
|
101
104
|
// Check if either CAMT or MT940 is supported for this account
|
|
102
|
-
return (this.config.isAccountTransactionSupported(
|
|
103
|
-
this.config.isAccountTransactionSupported(
|
|
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
|
|
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(
|
|
121
|
+
async getAccountStatements(account, from, to, preferCamt = true) {
|
|
119
122
|
// Check what formats the bank supports
|
|
120
|
-
const camtSupported = this.config.isAccountTransactionSupported(
|
|
121
|
-
const mt940Supported = this.config.isAccountTransactionSupported(
|
|
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 ${
|
|
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(
|
|
131
|
+
return (await this.startCustomerOrderInteraction(new StatementInteractionCAMT(account, from, to)));
|
|
129
132
|
}
|
|
130
133
|
else {
|
|
131
|
-
return (await this.startCustomerOrderInteraction(new StatementInteractionMT940(
|
|
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
|
|
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(
|
|
149
|
-
return
|
|
150
|
-
? this.config.isAccountTransactionSupported(
|
|
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
|
|
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(
|
|
162
|
-
return (await this.startCustomerOrderInteraction(new PortfolioInteraction(
|
|
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
|
|
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(
|
|
180
|
-
return
|
|
181
|
-
? this.config.isAccountTransactionSupported(
|
|
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
|
|
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(
|
|
193
|
-
return (await this.startCustomerOrderInteraction(new CreditCardStatementInteraction(
|
|
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
|
|
185
|
+
* @param account An account number, or an account from `bankingInformation.upd.bankAccounts`
|
|
163
186
|
* @param transId The transaction ID
|
|
164
187
|
*/
|
|
165
|
-
isAccountTransactionSupported(
|
|
166
|
-
const bankAccount = this.getBankAccount(
|
|
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
|
-
*
|
|
185
|
-
*
|
|
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(
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 {};
|
package/dist/httpClient.js
CHANGED
|
@@ -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';
|