lib-fints 1.3.1 → 1.4.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 (90) hide show
  1. package/README.md +43 -40
  2. package/dist/camtParser.js +412 -0
  3. package/dist/client.js +65 -82
  4. package/dist/dataGroups/SepaAccount.js +17 -0
  5. package/dist/dataGroups/SepaAccountParameters.js +16 -0
  6. package/dist/dialog.js +126 -108
  7. package/dist/httpClient.js +17 -3
  8. package/dist/index.js +1 -0
  9. package/dist/interactions/balanceInteraction.js +1 -1
  10. package/dist/interactions/customerInteraction.js +6 -3
  11. package/dist/interactions/endDialogInteraction.js +17 -0
  12. package/dist/interactions/initDialogInteraction.js +18 -8
  13. package/dist/interactions/sepaAccountInteraction.js +48 -0
  14. package/dist/interactions/statementInteractionCAMT.js +54 -0
  15. package/dist/interactions/{statementInteraction.js → statementInteractionMT940.js} +11 -8
  16. package/dist/interactions/tanMediaInteraction.js +4 -0
  17. package/dist/segments/HICAZ.js +22 -0
  18. package/dist/segments/HICAZS.js +19 -0
  19. package/dist/segments/HISPA.js +14 -0
  20. package/dist/segments/HISPAS.js +14 -0
  21. package/dist/segments/HKCAZ.js +28 -0
  22. package/dist/segments/HKSPA.js +21 -0
  23. package/dist/segments/registry.js +16 -2
  24. package/dist/statement.js +1 -0
  25. package/dist/tests/HICAZS.test.js +18 -0
  26. package/dist/tests/HKCAZ.test.js +47 -0
  27. package/dist/tests/camtParser.test.js +673 -0
  28. package/dist/tests/client.test.js +642 -23
  29. package/dist/tests/dialog.test.js +234 -0
  30. package/dist/types/bankAccount.d.ts +2 -3
  31. package/dist/types/bankAccount.d.ts.map +1 -1
  32. package/dist/types/camtParser.d.ts +31 -0
  33. package/dist/types/camtParser.d.ts.map +1 -0
  34. package/dist/types/client.d.ts +10 -10
  35. package/dist/types/client.d.ts.map +1 -1
  36. package/dist/types/config.d.ts +2 -1
  37. package/dist/types/config.d.ts.map +1 -1
  38. package/dist/types/dataGroups/SepaAccount.d.ts +11 -0
  39. package/dist/types/dataGroups/SepaAccount.d.ts.map +1 -0
  40. package/dist/types/dataGroups/SepaAccountParameters.d.ts +13 -0
  41. package/dist/types/dataGroups/SepaAccountParameters.d.ts.map +1 -0
  42. package/dist/types/dialog.d.ts +12 -8
  43. package/dist/types/dialog.d.ts.map +1 -1
  44. package/dist/types/httpClient.d.ts +2 -1
  45. package/dist/types/httpClient.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +2 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/interactions/customerInteraction.d.ts +7 -3
  49. package/dist/types/interactions/customerInteraction.d.ts.map +1 -1
  50. package/dist/types/interactions/endDialogInteraction.d.ts +13 -0
  51. package/dist/types/interactions/endDialogInteraction.d.ts.map +1 -0
  52. package/dist/types/interactions/initDialogInteraction.d.ts +2 -3
  53. package/dist/types/interactions/initDialogInteraction.d.ts.map +1 -1
  54. package/dist/types/interactions/sepaAccountInteraction.d.ts +17 -0
  55. package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -0
  56. package/dist/types/interactions/{statementInteraction.d.ts → statementInteractionCAMT.d.ts} +4 -7
  57. package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -0
  58. package/dist/types/interactions/statementInteractionMT940.d.ts +13 -0
  59. package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -0
  60. package/dist/types/interactions/tanMediaInteraction.d.ts.map +1 -1
  61. package/dist/types/mt940parser.d.ts +2 -38
  62. package/dist/types/mt940parser.d.ts.map +1 -1
  63. package/dist/types/segments/HICAZ.d.ts +22 -0
  64. package/dist/types/segments/HICAZ.d.ts.map +1 -0
  65. package/dist/types/segments/HICAZS.d.ts +17 -0
  66. package/dist/types/segments/HICAZS.d.ts.map +1 -0
  67. package/dist/types/segments/HISPA.d.ts +17 -0
  68. package/dist/types/segments/HISPA.d.ts.map +1 -0
  69. package/dist/types/segments/HISPAS.d.ts +14 -0
  70. package/dist/types/segments/HISPAS.d.ts.map +1 -0
  71. package/dist/types/segments/HKCAZ.d.ts +27 -0
  72. package/dist/types/segments/HKCAZ.d.ts.map +1 -0
  73. package/dist/types/segments/HKKAZ.d.ts +1 -1
  74. package/dist/types/segments/HKSAL.d.ts +1 -1
  75. package/dist/types/segments/HKSPA.d.ts +21 -0
  76. package/dist/types/segments/HKSPA.d.ts.map +1 -0
  77. package/dist/types/segments/HKTAN.d.ts +1 -1
  78. package/dist/types/segments/registry.d.ts.map +1 -1
  79. package/dist/types/statement.d.ts +39 -0
  80. package/dist/types/statement.d.ts.map +1 -0
  81. package/dist/types/tests/HICAZS.test.d.ts +2 -0
  82. package/dist/types/tests/HICAZS.test.d.ts.map +1 -0
  83. package/dist/types/tests/HKCAZ.test.d.ts +2 -0
  84. package/dist/types/tests/HKCAZ.test.d.ts.map +1 -0
  85. package/dist/types/tests/camtParser.test.d.ts +2 -0
  86. package/dist/types/tests/camtParser.test.d.ts.map +1 -0
  87. package/dist/types/tests/dialog.test.d.ts +2 -0
  88. package/dist/types/tests/dialog.test.d.ts.map +1 -0
  89. package/package.json +4 -1
  90. package/dist/types/interactions/statementInteraction.d.ts.map +0 -1
package/dist/client.js CHANGED
@@ -1,22 +1,22 @@
1
1
  import { Dialog } from './dialog.js';
2
- import { HKTAB } from './segments/HKTAB.js';
3
- import { StatementInteraction } from './interactions/statementInteraction.js';
2
+ import { StatementInteractionMT940 } from './interactions/statementInteractionMT940.js';
3
+ import { StatementInteractionCAMT } from './interactions/statementInteractionCAMT.js';
4
4
  import { BalanceInteraction } from './interactions/balanceInteraction.js';
5
5
  import { PortfolioInteraction } from './interactions/portfolioInteraction.js';
6
6
  import { FinTSConfig } from './config.js';
7
- import { TanMediaInteraction } from './interactions/tanMediaInteraction.js';
8
7
  import { HKSAL } from './segments/HKSAL.js';
9
8
  import { HKKAZ } from './segments/HKKAZ.js';
9
+ import { HKCAZ } from './segments/HKCAZ.js';
10
10
  import { HKWPD } from './segments/HKWPD.js';
11
11
  import { DKKKU } from './segments/DKKKU.js';
12
- import { InitDialogInteraction } from './interactions/initDialogInteraction.js';
13
- import { CreditCardStatementInteraction } from "./interactions/creditcardStatementInteraction.js";
12
+ import { CreditCardStatementInteraction } from './interactions/creditcardStatementInteraction.js';
13
+ import { HKIDN } from './segments/HKIDN.js';
14
14
  /**
15
15
  * A client to communicate with a bank over the FinTS protocol
16
16
  */
17
17
  export class FinTSClient {
18
18
  config;
19
- openCustomerInteractions = new Map();
19
+ currentDialog;
20
20
  /**
21
21
  * Creates a new FinTS client
22
22
  * @param config - the configuration for the client, use the static factory methods FinTSConfig.forFirstTimeUse or FinTSConfig.fromBankingInformation to create a configuration
@@ -51,21 +51,9 @@ export class FinTSClient {
51
51
  * @returns the synchronization response
52
52
  */
53
53
  async synchronize() {
54
- const dialog = new Dialog(this.config);
55
- const syncResponse = await this.initDialog(dialog, true);
56
- if (!syncResponse.success || syncResponse.requiresTan) {
57
- return syncResponse;
58
- }
59
- if (this.config.selectedTanMethod && this.config.isTransactionSupported(HKTAB.Id)) {
60
- const tanMediaResponse = await dialog.startCustomerOrderInteraction(new TanMediaInteraction());
61
- let tanMethod = this.config.selectedTanMethod;
62
- if (tanMethod) {
63
- tanMethod.activeTanMedia = tanMediaResponse.tanMediaList;
64
- }
65
- syncResponse.bankAnswers.push(...tanMediaResponse.bankAnswers);
66
- }
67
- await dialog.end();
68
- return syncResponse;
54
+ this.currentDialog = new Dialog(this.config, true);
55
+ const responses = await this.currentDialog.start();
56
+ return responses.get(HKIDN.Id);
69
57
  }
70
58
  /**
71
59
  * Continues the synchronization transaction when a TAN is required
@@ -74,7 +62,7 @@ export class FinTSClient {
74
62
  * @returns the synchronization response
75
63
  */
76
64
  async synchronizeWithTan(tanReference, tan) {
77
- return this.continueCustomerInteractionWithTan(tanReference, tan);
65
+ return await this.continueCustomerInteractionWithTan([HKIDN.Id], tanReference, tan);
78
66
  }
79
67
  /**
80
68
  * Checks if the bank supports fetching an account balance in general or for the given account number when provided
@@ -92,7 +80,8 @@ export class FinTSClient {
92
80
  * @returns the account balance response
93
81
  */
94
82
  async getAccountBalance(accountNumber) {
95
- return this.startCustomerOrderInteraction(new BalanceInteraction(accountNumber));
83
+ const response = await this.startCustomerOrderInteraction(new BalanceInteraction(accountNumber));
84
+ return response;
96
85
  }
97
86
  /**
98
87
  * Continues the account balance fetching when a TAN is required
@@ -101,7 +90,7 @@ export class FinTSClient {
101
90
  * @returns the account balance response
102
91
  */
103
92
  async getAccountBalanceWithTan(tanReference, tan) {
104
- return this.continueCustomerInteractionWithTan(tanReference, tan);
93
+ return (await this.continueCustomerInteractionWithTan([HKSAL.Id], tanReference, tan));
105
94
  }
106
95
  /**
107
96
  * Checks if the bank supports fetching account statements in general or for the given account number when provided
@@ -109,19 +98,39 @@ export class FinTSClient {
109
98
  * @returns true if the bank (and account) supports fetching account statements
110
99
  */
111
100
  canGetAccountStatements(accountNumber) {
112
- return accountNumber
113
- ? this.config.isAccountTransactionSupported(accountNumber, HKKAZ.Id)
114
- : this.config.isTransactionSupported(HKKAZ.Id);
101
+ if (accountNumber) {
102
+ // Check if either CAMT or MT940 is supported for this account
103
+ return (this.config.isAccountTransactionSupported(accountNumber, HKCAZ.Id) ||
104
+ this.config.isAccountTransactionSupported(accountNumber, HKKAZ.Id));
105
+ }
106
+ else {
107
+ // Check if either CAMT or MT940 is supported by the bank
108
+ return this.config.isTransactionSupported(HKCAZ.Id) || this.config.isTransactionSupported(HKKAZ.Id);
109
+ }
115
110
  }
116
111
  /**
117
112
  * Fetches the account statements for the given account number
118
113
  * @param accountNumber - the account number to fetch the statements for, must be an account available in the config.baningInformation.UPD.accounts
119
114
  * @param from - an optional start date of the period to fetch the statements for
120
115
  * @param to - an optional end date of the period to fetch the statements for
116
+ * @param preferCamt - whether to prefer CAMT format over MT940 when both are supported (default: true)
121
117
  * @returns an account statements response containing an array of statements
122
118
  */
123
- async getAccountStatements(accountNumber, from, to) {
124
- return this.startCustomerOrderInteraction(new StatementInteraction(accountNumber, from, to));
119
+ async getAccountStatements(accountNumber, from, to, preferCamt = true) {
120
+ // Check what formats the bank supports
121
+ const camtSupported = this.config.isAccountTransactionSupported(accountNumber, 'HKCAZ');
122
+ const mt940Supported = this.config.isAccountTransactionSupported(accountNumber, 'HKKAZ');
123
+ if (!camtSupported && !mt940Supported) {
124
+ throw Error(`Account ${accountNumber} does not support account statements`);
125
+ }
126
+ // Choose format based on support and preference
127
+ const useCAMT = (preferCamt && camtSupported) || (!mt940Supported && camtSupported);
128
+ if (useCAMT) {
129
+ return (await this.startCustomerOrderInteraction(new StatementInteractionCAMT(accountNumber, from, to)));
130
+ }
131
+ else {
132
+ return (await this.startCustomerOrderInteraction(new StatementInteractionMT940(accountNumber, from, to)));
133
+ }
125
134
  }
126
135
  /**
127
136
  * Continues the account statements fetching when a TAN is required
@@ -130,7 +139,7 @@ export class FinTSClient {
130
139
  * @returns an account statements response containing an array of statements
131
140
  */
132
141
  async getAccountStatementsWithTan(tanReference, tan) {
133
- return this.continueCustomerInteractionWithTan(tanReference, tan);
142
+ return (await this.continueCustomerInteractionWithTan([HKCAZ.Id, HKKAZ.Id], tanReference, tan));
134
143
  }
135
144
  /**
136
145
  * Checks if the bank supports fetching portfolio information in general or for the given account number when provided
@@ -151,17 +160,17 @@ export class FinTSClient {
151
160
  * @returns a portfolio response containing holdings and total value
152
161
  */
153
162
  async getPortfolio(accountNumber, currency, priceQuality, maxEntries) {
154
- return this.startCustomerOrderInteraction(new PortfolioInteraction(accountNumber, currency, priceQuality, maxEntries));
163
+ return (await this.startCustomerOrderInteraction(new PortfolioInteraction(accountNumber, currency, priceQuality, maxEntries)));
155
164
  }
156
165
  /**
157
- * Continues the portfolio fetching when a TAN is required
158
- * @param tanReference The TAN reference provided in the first call's response
159
-
160
- * @param tan The TAN entered by the user, can be omitted if a decoupled TAN method is used
161
- * @returns a portfolio response containing holdings and total value
162
- */
166
+ * Continues the portfolio fetching when a TAN is required
167
+ * @param tanReference The TAN reference provided in the first call's response
168
+
169
+ * @param tan The TAN entered by the user, can be omitted if a decoupled TAN method is used
170
+ * @returns a portfolio response containing holdings and total value
171
+ */
163
172
  async getPortfolioWithTan(tanReference, tan) {
164
- return this.continueCustomerInteractionWithTan(tanReference, tan);
173
+ return (await this.continueCustomerInteractionWithTan([HKWPD.Id], tanReference, tan));
165
174
  }
166
175
  /**
167
176
  * Checks if the bank supports fetching credit card statements in general or for the given account number
@@ -182,7 +191,7 @@ export class FinTSClient {
182
191
  * @returns an account statements response containing an array of statements
183
192
  */
184
193
  async getCreditCardStatements(accountNumber, from) {
185
- return this.startCustomerOrderInteraction(new CreditCardStatementInteraction(accountNumber, from));
194
+ return (await this.startCustomerOrderInteraction(new CreditCardStatementInteraction(accountNumber, from)));
186
195
  }
187
196
  /**
188
197
  * Continues the credit card statements fetching when a TAN is required
@@ -191,57 +200,31 @@ export class FinTSClient {
191
200
  * @returns a credit card statements response containing an array of statements
192
201
  */
193
202
  async getCreditCardStatementsWithTan(tanReference, tan) {
194
- return this.continueCustomerInteractionWithTan(tanReference, tan);
203
+ return (await this.continueCustomerInteractionWithTan([DKKKU.Id], tanReference, tan));
195
204
  }
196
205
  async startCustomerOrderInteraction(interaction) {
197
- const dialog = new Dialog(this.config);
198
- const syncResponse = await this.initDialog(dialog, false, interaction);
199
- if (!syncResponse.success || syncResponse.requiresTan) {
200
- return syncResponse;
201
- }
202
- const clientResponse = await dialog.startCustomerOrderInteraction(interaction);
203
- if (clientResponse.requiresTan) {
204
- this.openCustomerInteractions.set(clientResponse.tanReference, interaction);
206
+ this.currentDialog = new Dialog(this.config, false);
207
+ this.currentDialog.addCustomerInteraction(interaction);
208
+ const responses = await this.currentDialog.start();
209
+ const response = responses.get(interaction.segId);
210
+ if (response) {
211
+ return response;
205
212
  }
206
213
  else {
207
- await dialog.end();
214
+ return [...responses.values()].at(-1);
208
215
  }
209
- return clientResponse;
210
216
  }
211
- async continueCustomerInteractionWithTan(tanReference, tan) {
212
- const interaction = this.openCustomerInteractions.get(tanReference);
213
- if (!interaction) {
214
- throw new Error('No open customer interaction found for TAN reference: ' + tanReference);
215
- }
216
- const dialog = interaction.dialog;
217
- let responseMessage = await dialog.sendTanMessage(interaction.segId, tanReference, tan);
218
- let clientResponse = interaction.getClientResponse(responseMessage);
219
- this.openCustomerInteractions.delete(tanReference);
220
- if (!clientResponse.success) {
221
- await dialog.end();
222
- return clientResponse;
217
+ async continueCustomerInteractionWithTan(segIds, tanReference, tan) {
218
+ if (!this.currentDialog) {
219
+ throw new Error('no customer dialog was started which can continue');
223
220
  }
224
- if (clientResponse.requiresTan) {
225
- this.openCustomerInteractions.set(clientResponse.tanReference, interaction);
226
- return clientResponse;
227
- }
228
- const initDialogInteraction = interaction;
229
- if (initDialogInteraction.followUpInteraction) {
230
- clientResponse = await dialog.startCustomerOrderInteraction(initDialogInteraction.followUpInteraction);
231
- if (clientResponse.requiresTan) {
232
- this.openCustomerInteractions.set(clientResponse.tanReference, initDialogInteraction.followUpInteraction);
233
- return clientResponse;
221
+ const responses = await this.currentDialog.continue(tanReference, tan);
222
+ for (const segId of segIds) {
223
+ const response = responses.get(segId);
224
+ if (response) {
225
+ return response;
234
226
  }
235
227
  }
236
- await dialog.end();
237
- return clientResponse;
238
- }
239
- async initDialog(dialog, syncSystemId = false, followUpInteraction) {
240
- const interaction = new InitDialogInteraction(this.config, syncSystemId, followUpInteraction);
241
- const initResponse = await dialog.initialize(interaction);
242
- if (initResponse.requiresTan) {
243
- this.openCustomerInteractions.set(initResponse.tanReference, interaction);
244
- }
245
- return initResponse;
228
+ return [...responses.values()].at(-1);
246
229
  }
247
230
  }
@@ -0,0 +1,17 @@
1
+ import { AlphaNumeric } from '../dataElements/AlphaNumeric.js';
2
+ import { DataGroup } from './DataGroup.js';
3
+ import { Identification } from '../dataElements/Identification.js';
4
+ import { BankIdentification } from './BankIdentification.js';
5
+ import { YesNo } from '../dataElements/YesNo.js';
6
+ export class SepaAccountGroup extends DataGroup {
7
+ constructor(name, minCount = 0, maxCount = 1, minVersion, maxVersion) {
8
+ super(name, [
9
+ new YesNo('isSepaAccount', 1, 1),
10
+ new AlphaNumeric('iban', 0, 1, 34),
11
+ new AlphaNumeric('bic', 0, 1, 11),
12
+ new Identification('accountNumber', 1, 1),
13
+ new Identification('subAccountId', 0, 1),
14
+ new BankIdentification('bank', 1, 1),
15
+ ], minCount, maxCount, minVersion, maxVersion);
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ import { YesNo } from '../dataElements/YesNo.js';
2
+ import { Numeric } from '../dataElements/Numeric.js';
3
+ import { AlphaNumeric } from '../dataElements/AlphaNumeric.js';
4
+ import { DataGroup } from './DataGroup.js';
5
+ export class SepaAccountParametersGroup extends DataGroup {
6
+ constructor(name, minCount = 1, maxCount = 1, minVersion, maxVersion) {
7
+ super(name, [
8
+ new YesNo('individualAccountRetrievalAllowed', 1, 1),
9
+ new YesNo('nationalAccountAllowed', 1, 1),
10
+ new YesNo('structuredPurposeAllowed', 1, 1),
11
+ new YesNo('maxEntriesAllowed', 1, 1, 2), // version 2+
12
+ new Numeric('reservedPurposePositions', 1, 1, 2, 3), // version 3+
13
+ new AlphaNumeric('supportedSepaFormats', 0, 99, 256), // optional, up to 99 entries
14
+ ], minCount, maxCount, minVersion, maxVersion);
15
+ }
16
+ }
package/dist/dialog.js CHANGED
@@ -1,47 +1,142 @@
1
1
  import { TanMediaRequirement, TanProcess } from './codes.js';
2
2
  import { HttpClient } from './httpClient.js';
3
3
  import { CustomerMessage, CustomerOrderMessage } from './message.js';
4
- import { HKEND } from './segments/HKEND.js';
5
4
  import { HKIDN } from './segments/HKIDN.js';
6
5
  import { HKTAN } from './segments/HKTAN.js';
7
6
  import { HNHBK } from './segments/HNHBK.js';
8
7
  import { decode } from './segment.js';
9
8
  import { PARTED } from './partedSegment.js';
9
+ import { CustomerOrderInteraction } from './interactions/customerInteraction.js';
10
+ import { InitDialogInteraction } from './interactions/initDialogInteraction.js';
11
+ import { EndDialogInteraction } from './interactions/endDialogInteraction.js';
10
12
  export class Dialog {
11
13
  config;
12
14
  dialogId = '0';
13
15
  lastMessageNumber = 0;
16
+ interactions = [];
17
+ responses = new Map();
18
+ currentInteractionIndex = 0;
14
19
  isInitialized = false;
15
20
  hasEnded = false;
16
21
  httpClient;
17
- constructor(config) {
22
+ constructor(config, syncSystemId = false) {
18
23
  this.config = config;
19
24
  if (!this.config) {
20
25
  throw new Error('configuration must be provided');
21
26
  }
22
27
  this.httpClient = this.getHttpClient();
28
+ this.interactions.push(new InitDialogInteraction(this.config, syncSystemId));
29
+ this.interactions.push(new EndDialogInteraction());
30
+ this.interactions.forEach((interaction) => {
31
+ interaction.dialog = this;
32
+ });
23
33
  }
24
- async initialize(interaction) {
34
+ get currentInteraction() {
35
+ return this.interactions[this.currentInteractionIndex];
36
+ }
37
+ async start() {
25
38
  if (this.isInitialized) {
26
39
  throw new Error('dialog has already been initialized');
27
40
  }
28
41
  if (this.hasEnded) {
29
- throw Error('cannot initialize a dialog that has already ended');
42
+ throw Error('cannot start a dialog that has already ended');
30
43
  }
31
44
  if (this.lastMessageNumber > 0) {
32
- throw new Error('dialog initialization must be the first message in a dialog');
45
+ throw new Error('dialog start can only be called on a new dialog');
46
+ }
47
+ let clientResponse;
48
+ do {
49
+ const message = this.createCurrentCustomerMessage();
50
+ const responseMessage = await this.httpClient.sendMessage(message);
51
+ await this.handlePartedMessages(message, responseMessage, this.currentInteraction);
52
+ clientResponse = this.currentInteraction.handleClientResponse(responseMessage);
53
+ this.checkEnded(clientResponse);
54
+ this.dialogId = clientResponse.dialogId;
55
+ this.responses.set(this.currentInteraction.segId, clientResponse);
56
+ if (clientResponse.success && !clientResponse.requiresTan) {
57
+ this.currentInteractionIndex++;
58
+ if (this.currentInteractionIndex > 0) {
59
+ this.isInitialized = true;
60
+ }
61
+ }
62
+ } while (!this.hasEnded &&
63
+ this.currentInteractionIndex < this.interactions.length &&
64
+ clientResponse.success &&
65
+ !clientResponse.requiresTan);
66
+ return this.responses;
67
+ }
68
+ async continue(tanOrderReference, tan) {
69
+ if (!tanOrderReference) {
70
+ throw Error('tanOrderReference must be provided to continue a customer order with a TAN');
71
+ }
72
+ if (!this.config.selectedTanMethod?.isDecoupled && !tan) {
73
+ throw Error('TAN must be provided for non-decoupled TAN methods');
74
+ }
75
+ if (this.hasEnded) {
76
+ throw Error('cannot continue a customer order when dialog has already ended');
77
+ }
78
+ if (!this.currentInteraction) {
79
+ throw new Error('there is no running customer interaction in this dialog to continue');
80
+ }
81
+ let clientResponse;
82
+ let isFirstMessage = true;
83
+ do {
84
+ const message = isFirstMessage
85
+ ? this.createCurrentTanMessage(tanOrderReference, tan)
86
+ : this.createCurrentCustomerMessage();
87
+ const responseMessage = await this.httpClient.sendMessage(message);
88
+ await this.handlePartedMessages(message, responseMessage, this.currentInteraction);
89
+ clientResponse = this.currentInteraction.handleClientResponse(responseMessage);
90
+ this.checkEnded(clientResponse);
91
+ this.dialogId = clientResponse.dialogId;
92
+ this.responses.set(this.currentInteraction.segId, clientResponse);
93
+ if (clientResponse.success && !clientResponse.requiresTan) {
94
+ this.currentInteractionIndex++;
95
+ if (this.currentInteractionIndex > 0) {
96
+ this.isInitialized = true;
97
+ }
98
+ }
99
+ isFirstMessage = false;
100
+ } while (!this.hasEnded &&
101
+ this.currentInteractionIndex < this.interactions.length &&
102
+ clientResponse.success &&
103
+ !clientResponse.requiresTan);
104
+ return this.responses;
105
+ }
106
+ addCustomerInteraction(interaction, afterCurrent = false) {
107
+ if (this.hasEnded) {
108
+ throw Error('cannot queue another customer interaction when dialog has already ended');
109
+ }
110
+ const isCustomerOrder = interaction instanceof CustomerOrderInteraction;
111
+ if (isCustomerOrder && !this.config.isTransactionSupported(interaction.segId)) {
112
+ throw Error(`customer order transaction ${interaction.segId} is not supported according to the BPD`);
33
113
  }
34
114
  interaction.dialog = this;
115
+ if (afterCurrent) {
116
+ this.interactions.splice(this.currentInteractionIndex + 1, 0, interaction);
117
+ return;
118
+ }
119
+ this.interactions.splice(this.interactions.length - 1, 0, interaction);
120
+ }
121
+ createCurrentCustomerMessage() {
35
122
  this.lastMessageNumber++;
36
- const message = new CustomerMessage(this.dialogId, this.lastMessageNumber);
123
+ const isCustomerOrder = this.currentInteraction instanceof CustomerOrderInteraction;
124
+ const message = isCustomerOrder
125
+ ? new CustomerOrderMessage(this.currentInteraction.segId, this.currentInteraction.responseSegId, this.dialogId, this.lastMessageNumber)
126
+ : new CustomerMessage(this.dialogId, this.lastMessageNumber);
37
127
  const tanMethod = this.config.selectedTanMethod;
38
- const isScaSupported = tanMethod && tanMethod.version >= 6;
128
+ let isScaSupported = tanMethod && tanMethod.version >= 6;
129
+ let isTanMethodNeeded = isScaSupported;
130
+ if (isCustomerOrder) {
131
+ const bankTransaction = this.config.bankingInformation.bpd?.allowedTransactions.find((t) => t.transId === this.currentInteraction.segId);
132
+ isTanMethodNeeded = isScaSupported && bankTransaction?.tanRequired;
133
+ }
39
134
  if (this.config.userId && this.config.pin) {
40
135
  message.sign(this.config.countryCode, this.config.bankId, this.config.userId, this.config.pin, this.config.bankingInformation.systemId, isScaSupported ? this.config.tanMethodId : undefined);
41
136
  }
42
- const segments = interaction.getSegments(this.config);
137
+ const segments = this.currentInteraction.getSegments(this.config);
43
138
  segments.forEach((segment) => message.addSegment(segment));
44
- if (this.config.userId && this.config.pin && isScaSupported) {
139
+ if (this.config.userId && this.config.pin && isTanMethodNeeded) {
45
140
  const hktan = {
46
141
  header: { segId: HKTAN.Id, segNr: 0, version: tanMethod.version },
47
142
  tanProcess: TanProcess.Process4,
@@ -49,76 +144,35 @@ export class Dialog {
49
144
  };
50
145
  message.addSegment(hktan);
51
146
  }
52
- const initResponse = await this.httpClient.sendMessage(message);
53
- const clientResponse = interaction.getClientResponse(initResponse);
54
- this.dialogId = clientResponse.dialogId;
55
- if (clientResponse.success) {
56
- this.isInitialized = true;
57
- }
58
- this.checkEnded(initResponse);
59
- return clientResponse;
147
+ return message;
60
148
  }
61
- async end() {
62
- if (!this.isInitialized || this.hasEnded) {
63
- return true;
64
- }
65
- const tanMethod = this.config.selectedTanMethod;
66
- const isScaSupported = tanMethod && tanMethod.version >= 6;
67
- if (this.config.tanMethodId && !tanMethod) {
68
- throw new Error('given tanMethodId is not available according to the BPD');
69
- }
149
+ createCurrentTanMessage(tanOrderReference, tan) {
70
150
  this.lastMessageNumber++;
71
151
  const message = new CustomerMessage(this.dialogId, this.lastMessageNumber);
72
152
  if (this.config.userId && this.config.pin) {
73
- message.sign(this.config.countryCode, this.config.bankId, this.config.userId, this.config.pin, this.config.bankingInformation.systemId, isScaSupported ? this.config.tanMethodId : undefined);
74
- }
75
- const hkend = {
76
- header: { segId: HKEND.Id, segNr: 0, version: HKEND.Version },
77
- dialogId: this.dialogId,
78
- };
79
- message.addSegment(hkend);
80
- const responseMessage = await this.httpClient.sendMessage(message);
81
- this.checkEnded(responseMessage);
82
- return this.hasEnded;
83
- }
84
- async startCustomerOrderInteraction(interaction) {
85
- if (!this.isInitialized) {
86
- throw Error('dialog must be initialized before sending a customer order');
87
- }
88
- if (this.hasEnded) {
89
- throw Error('cannot send a customer message when dialog has already ended');
90
- }
91
- const bankTransaction = this.config.bankingInformation.bpd?.allowedTransactions.find((t) => t.transId === interaction.segId);
92
- if (!bankTransaction) {
93
- throw Error(`transaction ${interaction.segId} is not supported according to the BPD`);
94
- }
95
- interaction.dialog = this;
96
- this.lastMessageNumber++;
97
- const message = new CustomerOrderMessage(interaction.segId, interaction.responseSegId, this.dialogId, this.lastMessageNumber);
98
- if (this.config.userId && this.config.pin) {
99
- message.sign(this.config.countryCode, this.config.bankId, this.config.userId, this.config.pin, this.config.bankingInformation.systemId, this.config.selectedTanMethod && (bankTransaction.tanRequired || this.config.selectedTanMethod.version >= 6)
100
- ? this.config.tanMethodId
101
- : undefined);
153
+ message.sign(this.config.countryCode, this.config.bankId, this.config.userId, this.config.pin, this.config.bankingInformation.systemId, this.config.tanMethodId, tan);
102
154
  }
103
- const segments = interaction.getSegments(this.config);
104
- segments.forEach((segment) => message.addSegment(segment));
105
- if (bankTransaction.tanRequired) {
106
- if (this.config.userId && this.config.pin && this.config.tanMethodId) {
107
- const hktan = {
108
- header: { segId: HKTAN.Id, segNr: 0, version: this.config.selectedTanMethod.version },
109
- tanProcess: TanProcess.Process4,
110
- segId: interaction.segId,
111
- tanMedia: this.config.tanMediaName,
112
- };
113
- message.addSegment(hktan);
114
- }
155
+ if (this.config.userId && this.config.pin && this.config.tanMethodId) {
156
+ const hktan = {
157
+ header: { segId: HKTAN.Id, segNr: 0, version: this.config.selectedTanMethod.version },
158
+ tanProcess: this.config.selectedTanMethod?.isDecoupled ? TanProcess.Status : TanProcess.Process2,
159
+ segId: this.currentInteraction.segId,
160
+ orderRef: tanOrderReference,
161
+ nextTan: false,
162
+ tanMedia: this.config.selectedTanMethod.tanMediaRequirement >= TanMediaRequirement.Optional
163
+ ? this.config.tanMediaName
164
+ : undefined,
165
+ };
166
+ message.addSegment(hktan);
115
167
  }
116
- let responseMessage = await this.httpClient.sendMessage(message);
168
+ return message;
169
+ }
170
+ async handlePartedMessages(message, responseMessage, interaction) {
117
171
  let partedSegment = responseMessage.findSegment(PARTED.Id);
118
172
  if (partedSegment) {
119
173
  while (responseMessage.hasReturnCode(3040)) {
120
174
  const answers = responseMessage.getBankAnswers();
121
- const segmentWithContinuation = segments.find((s) => s.header.segId === interaction.segId);
175
+ const segmentWithContinuation = message.segments.find((s) => s.header.segId === interaction.segId);
122
176
  if (!segmentWithContinuation) {
123
177
  throw new Error(`Response contains segment with further information, but corresponding segment could not be found or is not specified`);
124
178
  }
@@ -137,46 +191,10 @@ export class Dialog {
137
191
  const index = responseMessage.segments.indexOf(partedSegment);
138
192
  responseMessage.segments.splice(index, 1, completeSegment);
139
193
  }
140
- this.checkEnded(responseMessage);
141
- return interaction.getClientResponse(responseMessage);
142
- }
143
- async sendTanMessage(refSegId, tanOrderReference, tan) {
144
- if (!refSegId || !tanOrderReference) {
145
- throw Error('refSegId and tanOrderReference must be provided to send a TAN message');
146
- }
147
- if (!this.config.selectedTanMethod?.isDecoupled && !tan) {
148
- throw Error('TAN must be provided for non-decoupled TAN methods');
149
- }
150
- if (!this.isInitialized) {
151
- throw Error('dialog must be initialized before sending a TAN message');
152
- }
153
- if (this.hasEnded) {
154
- throw Error('cannot send a TAN message when dialog has alreay ended');
155
- }
156
- this.lastMessageNumber++;
157
- const message = new CustomerMessage(this.dialogId, this.lastMessageNumber);
158
- if (this.config.userId && this.config.pin) {
159
- message.sign(this.config.countryCode, this.config.bankId, this.config.userId, this.config.pin, this.config.bankingInformation.systemId, this.config.tanMethodId, tan);
160
- }
161
- if (this.config.userId && this.config.pin && this.config.tanMethodId) {
162
- const hktan = {
163
- header: { segId: HKTAN.Id, segNr: 0, version: this.config.selectedTanMethod.version },
164
- tanProcess: this.config.selectedTanMethod?.isDecoupled ? TanProcess.Status : TanProcess.Process2,
165
- segId: refSegId,
166
- orderRef: tanOrderReference,
167
- nextTan: false,
168
- tanMedia: this.config.selectedTanMethod.tanMediaRequirement >= TanMediaRequirement.Optional
169
- ? this.config.tanMediaName
170
- : undefined,
171
- };
172
- message.addSegment(hktan);
173
- }
174
- const responseMessage = await this.httpClient.sendMessage(message);
175
- this.checkEnded(responseMessage);
176
- return responseMessage;
177
194
  }
178
- checkEnded(initResponse) {
179
- if (initResponse.hasReturnCode(100) || initResponse.hasReturnCode(9800)) {
195
+ checkEnded(response) {
196
+ if (response.bankAnswers.some((answer) => answer.code === 100) ||
197
+ response.bankAnswers.some((answer) => answer.code === 9000)) {
180
198
  this.hasEnded = true;
181
199
  }
182
200
  }
@@ -2,15 +2,23 @@ import { Message } from './message.js';
2
2
  export class HttpClient {
3
3
  url;
4
4
  debug;
5
- constructor(url, debug = false) {
5
+ debugRaw;
6
+ constructor(url, debug = false, debugRaw = false) {
6
7
  this.url = url;
7
8
  this.debug = debug;
9
+ this.debugRaw = debugRaw;
8
10
  }
9
11
  async sendMessage(message) {
10
12
  const encodedMessage = message.encode();
11
13
  const requestBuffer = Buffer.from(encodedMessage);
12
14
  if (this.debug) {
13
- console.log('Request Message:\n' + message.toString());
15
+ console.log('Request Message:\n');
16
+ if (this.debugRaw) {
17
+ console.log(encodedMessage.split("'").join('\n'));
18
+ }
19
+ else {
20
+ console.log(message.toString());
21
+ }
14
22
  }
15
23
  const response = await fetch(this.url, {
16
24
  method: 'POST',
@@ -24,7 +32,13 @@ export class HttpClient {
24
32
  const customerOrderMessage = message;
25
33
  const responseMessage = Message.decode(responseText, customerOrderMessage.supportsPartedResponseSegments ? customerOrderMessage.orderResponseSegId : undefined);
26
34
  if (this.debug) {
27
- console.log('Response Message:\n' + responseMessage.toString(true) + '\n');
35
+ console.log('Response Message:\n');
36
+ if (this.debugRaw) {
37
+ console.log(responseText.split("'").join('\n'));
38
+ }
39
+ else {
40
+ console.log('Response Message:\n' + responseMessage.toString(true) + '\n');
41
+ }
28
42
  }
29
43
  return responseMessage;
30
44
  }
package/dist/index.js CHANGED
@@ -12,5 +12,6 @@ export * from './bankingInformation.js';
12
12
  export * from './accountBalance.js';
13
13
  export * from './bpd.js';
14
14
  export * from './upd.js';
15
+ export * from './statement.js';
15
16
  export * from './mt940parser.js';
16
17
  export * from './mt535parser.js';
@@ -13,11 +13,11 @@ export class BalanceInteraction extends CustomerOrderInteraction {
13
13
  if (!init.isAccountTransactionSupported(this.accountNumber, this.segId)) {
14
14
  throw Error(`Account ${this.accountNumber} does not support business transaction '${this.segId}'`);
15
15
  }
16
- const account = { ...bankAccount, iban: undefined };
17
16
  const version = init.getMaxSupportedTransactionVersion(HKSAL.Id);
18
17
  if (!version) {
19
18
  throw Error(`There is no supported version for business transaction '${HKSAL.Id}`);
20
19
  }
20
+ const account = version <= 6 ? { ...bankAccount, iban: undefined, bic: undefined } : bankAccount;
21
21
  const hksal = {
22
22
  header: { segId: HKSAL.Id, segNr: 0, version: version },
23
23
  account,