lib-fints 1.4.7 → 1.5.0

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 (58) hide show
  1. package/README.md +3 -1
  2. package/dist/client.js +37 -0
  3. package/dist/dataElements/Binary.js +18 -1
  4. package/dist/dialog.js +64 -35
  5. package/dist/electronicStatement.js +1 -0
  6. package/dist/httpClient.js +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/interactions/creditcardStatementInteraction.js +9 -1
  9. package/dist/interactions/electronicStatementInteraction.js +88 -0
  10. package/dist/interactions/initDialogInteraction.js +3 -1
  11. package/dist/interactions/portfolioInteraction.js +10 -4
  12. package/dist/interactions/sepaAccountInteraction.js +5 -3
  13. package/dist/interactions/statementInteractionCAMT.js +8 -3
  14. package/dist/interactions/statementInteractionMT940.js +10 -3
  15. package/dist/message.js +5 -1
  16. package/dist/segments/HIEKA.js +45 -0
  17. package/dist/segments/HIEKAS.js +18 -0
  18. package/dist/segments/HKCAZ.js +2 -2
  19. package/dist/segments/HKEKA.js +43 -0
  20. package/dist/segments/registry.js +6 -0
  21. package/dist/tests/HKEKA.test.js +198 -0
  22. package/dist/tests/partedResponse.test.js +181 -0
  23. package/dist/types/client.d.ts +28 -0
  24. package/dist/types/client.d.ts.map +1 -1
  25. package/dist/types/dataElements/Binary.d.ts +7 -0
  26. package/dist/types/dataElements/Binary.d.ts.map +1 -1
  27. package/dist/types/dialog.d.ts +15 -0
  28. package/dist/types/dialog.d.ts.map +1 -1
  29. package/dist/types/electronicStatement.d.ts +40 -0
  30. package/dist/types/electronicStatement.d.ts.map +1 -0
  31. package/dist/types/index.d.ts +3 -0
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/interactions/creditcardStatementInteraction.d.ts.map +1 -1
  34. package/dist/types/interactions/electronicStatementInteraction.d.ts +33 -0
  35. package/dist/types/interactions/electronicStatementInteraction.d.ts.map +1 -0
  36. package/dist/types/interactions/initDialogInteraction.d.ts.map +1 -1
  37. package/dist/types/interactions/portfolioInteraction.d.ts.map +1 -1
  38. package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -1
  39. package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -1
  40. package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -1
  41. package/dist/types/message.d.ts.map +1 -1
  42. package/dist/types/segments/HIEKA.d.ts +52 -0
  43. package/dist/types/segments/HIEKA.d.ts.map +1 -0
  44. package/dist/types/segments/HIEKAS.d.ts +21 -0
  45. package/dist/types/segments/HIEKAS.d.ts.map +1 -0
  46. package/dist/types/segments/HKCAZ.d.ts +2 -2
  47. package/dist/types/segments/HKCAZ.d.ts.map +1 -1
  48. package/dist/types/segments/HKEKA.d.ts +54 -0
  49. package/dist/types/segments/HKEKA.d.ts.map +1 -0
  50. package/dist/types/segments/registry.d.ts.map +1 -1
  51. package/dist/types/tests/HKEKA.test.d.ts +2 -0
  52. package/dist/types/tests/HKEKA.test.d.ts.map +1 -0
  53. package/dist/types/tests/partedResponse.test.d.ts +2 -0
  54. package/dist/types/tests/partedResponse.test.d.ts.map +1 -0
  55. package/package.json +5 -5
  56. package/dist/dataGroups/CamtAccount.js +0 -7
  57. package/dist/types/dataGroups/CamtAccount.d.ts +0 -9
  58. package/dist/types/dataGroups/CamtAccount.d.ts.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Lib-FinTS
4
4
 
5
- A Typescript/Javascript client library for Online-Banking via the FinTS 3.0 protocol with PIN/TAN, supporting PSD2 and decoupled TAN methods. The library has no dependencies on other libraries
5
+ A Typescript/Javascript client library for Online-Banking via the FinTS 3.0 protocol with PIN/TAN, supporting PSD2 and decoupled TAN methods.
6
6
 
7
7
  ## Getting Started
8
8
 
@@ -201,6 +201,7 @@ The following table shows all transactions supported by the FinTSClient interfac
201
201
  | **Account Statements** | `getAccountStatements(accountNumber, from?, to?)` | Fetches account transactions/statements for a date range (MT940 or CAMT format) | HKKAZ, HKCAZ | ✓ | ✓ |
202
202
  | **Portfolio** | `getPortfolio(accountNumber, currency?, priceQuality?, maxEntries?)` | Fetches securities portfolio information for depot accounts | HKWPD | ✓ | ✓ |
203
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 | ✓ | ✓ |
204
205
  | **TAN Method Selection** | `selectTanMethod(tanMethodId)` | Selects a TAN method by ID from available methods | - | ❌ | ❌ |
205
206
  | **TAN Media Selection** | `selectTanMedia(tanMediaName)` | Selects a specific TAN media device by name | - | ❌ | ❌ |
206
207
 
@@ -214,6 +215,7 @@ For each account-specific transaction, the client provides corresponding `can*`
214
215
  | `canGetAccountStatements(accountNumber?)` | Checks if account statements fetching is supported (MT940/CAMT) |
215
216
  | `canGetPortfolio(accountNumber?)` | Checks if portfolio information fetching is supported |
216
217
  | `canGetCreditCardStatements(accountNumber?)` | Checks if credit card statements fetching is supported |
218
+ | `canGetElectronicStatements(accountNumber?)` | Checks if electronic account statements fetching is supported |
217
219
 
218
220
  ### Transaction Parameters
219
221
 
package/dist/client.js CHANGED
@@ -2,11 +2,13 @@ import { FinTSConfig } from './config.js';
2
2
  import { Dialog } from './dialog.js';
3
3
  import { BalanceInteraction, } from './interactions/balanceInteraction.js';
4
4
  import { CreditCardStatementInteraction } from './interactions/creditcardStatementInteraction.js';
5
+ import { ElectronicStatementInteraction, } from './interactions/electronicStatementInteraction.js';
5
6
  import { PortfolioInteraction, } from './interactions/portfolioInteraction.js';
6
7
  import { StatementInteractionCAMT } from './interactions/statementInteractionCAMT.js';
7
8
  import { StatementInteractionMT940 } from './interactions/statementInteractionMT940.js';
8
9
  import { DKKKU } from './segments/DKKKU.js';
9
10
  import { HKCAZ } from './segments/HKCAZ.js';
11
+ import { HKEKA } from './segments/HKEKA.js';
10
12
  import { HKIDN } from './segments/HKIDN.js';
11
13
  import { HKKAZ } from './segments/HKKAZ.js';
12
14
  import { HKSAL } from './segments/HKSAL.js';
@@ -201,6 +203,41 @@ export class FinTSClient {
201
203
  async getCreditCardStatementsWithTan(tanReference, tan) {
202
204
  return (await this.continueCustomerInteractionWithTan([DKKKU.Id], tanReference, tan));
203
205
  }
206
+ /**
207
+ * 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
+ * @returns true if the bank (and account) supports fetching electronic account statements
210
+ */
211
+ canGetElectronicStatements(accountNumber) {
212
+ return accountNumber
213
+ ? this.config.isAccountTransactionSupported(accountNumber, HKEKA.Id)
214
+ : this.config.isTransactionSupported(HKEKA.Id);
215
+ }
216
+ /**
217
+ * Fetches an electronic account statement (Elektronischer Kontoauszug) for the given account number
218
+ *
219
+ * This returns the statement document the bank files in the customer's electronic mailbox,
220
+ * usually a PDF, not a list of transactions. The bank hands out one statement per call and
221
+ * announces a waiting successor in `nextOffset`; pass that value back in `options.offset` to
222
+ * fetch the next one. Banks that set `receiptRequired` in their HIEKAS parameters keep
223
+ * offering a statement until it has been acknowledged with its receipt.
224
+ *
225
+ * @param accountNumber - the account number to fetch the statement for, must be an account available in the config.bankingInformation.upd.accounts
226
+ * @param options - optional format, statement number and year, entry limit and offset
227
+ * @returns a response containing the statement documents and the offset of a waiting successor
228
+ */
229
+ async getElectronicStatements(accountNumber, options) {
230
+ return (await this.startCustomerOrderInteraction(new ElectronicStatementInteraction(accountNumber, options)));
231
+ }
232
+ /**
233
+ * Continues the electronic account statement fetching when a TAN is required
234
+ * @param tanReference The TAN reference provided in the first call's response
235
+ * @param tan The TAN entered by the user, can be omitted if a decoupled TAN method is used
236
+ * @returns a response containing the statement documents
237
+ */
238
+ async getElectronicStatementsWithTan(tanReference, tan) {
239
+ return (await this.continueCustomerInteractionWithTan([HKEKA.Id], tanReference, tan));
240
+ }
204
241
  async startCustomerOrderInteraction(interaction) {
205
242
  this.currentDialog = new Dialog(this.config, false);
206
243
  this.currentDialog.addCustomerInteraction(interaction);
@@ -14,7 +14,24 @@ export class Binary extends DataElement {
14
14
  }
15
15
  return `@${value.length}@${value}`;
16
16
  }
17
+ /**
18
+ * A binary value arrives as `@<length>@<data>`. The length is authoritative: it is the
19
+ * only thing that tells data apart from the separators and escape characters that a
20
+ * binary payload — a PDF, for instance — is full of. Returning everything after the
21
+ * second `@` instead would hand out whatever the bank appended between the end of the
22
+ * data and the next separator.
23
+ */
17
24
  decode(text) {
18
- return text.slice(text.indexOf('@', 1) + 1);
25
+ if (text[0] !== '@') {
26
+ // Not length-prefixed — nothing to go by, take it as it is.
27
+ return text;
28
+ }
29
+ const lengthEnd = text.indexOf('@', 1);
30
+ if (lengthEnd < 0) {
31
+ return text;
32
+ }
33
+ const dataStart = lengthEnd + 1;
34
+ const length = Number.parseInt(text.slice(1, lengthEnd), 10);
35
+ return Number.isNaN(length) ? text.slice(dataStart) : text.slice(dataStart, dataStart + length);
19
36
  }
20
37
  }
package/dist/dialog.js CHANGED
@@ -143,6 +143,7 @@ export class Dialog {
143
143
  header: { segId: HKTAN.Id, segNr: 0, version: tanMethod?.version ?? 0 },
144
144
  tanProcess: TanProcess.Process4,
145
145
  segId: this.currentInteraction.segId,
146
+ tanMedia: this.getTanMediaName(),
146
147
  };
147
148
  message.addSegment(hktan);
148
149
  }
@@ -163,48 +164,76 @@ export class Dialog {
163
164
  segId: this.currentInteraction.segId,
164
165
  orderRef: tanOrderReference,
165
166
  nextTan: false,
166
- tanMedia: (this.config.selectedTanMethod?.tanMediaRequirement ??
167
- TanMediaRequirement.NotAllowed >= TanMediaRequirement.Optional)
168
- ? this.config.tanMediaName
169
- : undefined,
167
+ tanMedia: this.getTanMediaName(),
170
168
  };
171
169
  message.addSegment(hktan);
172
170
  }
173
171
  return message;
174
172
  }
173
+ getTanMediaName() {
174
+ const requirement = this.config.selectedTanMethod?.tanMediaRequirement ?? TanMediaRequirement.NotAllowed;
175
+ if (requirement === TanMediaRequirement.NotAllowed) {
176
+ return undefined;
177
+ }
178
+ if (requirement === TanMediaRequirement.Required) {
179
+ return this.config.tanMediaName ?? 'default';
180
+ }
181
+ return this.config.tanMediaName;
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
+ */
175
197
  async handlePartedMessages(message, responseMessage, interaction) {
176
- let partedSegment = responseMessage.findSegment(PARTED.Id);
177
- if (partedSegment) {
178
- while (responseMessage.hasReturnCode(3040)) {
179
- const answers = responseMessage.getBankAnswers();
180
- const segmentWithContinuation = message.segments.find((s) => s.header.segId === interaction.segId);
181
- if (!segmentWithContinuation) {
182
- throw new Error(`Response contains segment with further information, but corresponding segment could not be found or is not specified`);
183
- }
184
- const answer = answers.find((a) => a.code === 3040);
185
- if (!answer || !answer.params || answer.params.length === 0) {
186
- throw new Error('Expected bank answer to contain continuation mark parameters (code 3040)');
187
- }
188
- segmentWithContinuation.continuationMark = answer.params[0];
189
- const hnhbkSegment = message.findSegment(HNHBK.Id);
190
- if (!hnhbkSegment) {
191
- throw new Error('HNHBK segment not found in message');
192
- }
193
- hnhbkSegment.msgNr = ++this.lastMessageNumber;
194
- const nextResponseMessage = await this.httpClient.sendMessage(message);
195
- const nextPartedSegment = nextResponseMessage.findSegment(PARTED.Id);
196
- if (nextPartedSegment) {
197
- nextPartedSegment.rawData =
198
- partedSegment.rawData +
199
- nextPartedSegment.rawData.slice(nextPartedSegment.rawData.indexOf('+') + 1);
200
- partedSegment = nextPartedSegment;
201
- }
202
- responseMessage = nextResponseMessage;
203
- }
204
- const completeSegment = decode(partedSegment.rawData);
205
- const index = responseMessage.segments.indexOf(partedSegment);
206
- 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;
207
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;
208
237
  }
209
238
  checkEnded(response) {
210
239
  if (response.bankAnswers.some((answer) => answer.code === 100) ||
@@ -0,0 +1 @@
1
+ export {};
@@ -10,7 +10,7 @@ export class HttpClient {
10
10
  }
11
11
  async sendMessage(message) {
12
12
  const encodedMessage = message.encode();
13
- const requestBuffer = Buffer.from(encodedMessage);
13
+ const requestBuffer = Buffer.from(encodedMessage, 'latin1');
14
14
  if (this.debug) {
15
15
  console.log('Request Message:\n');
16
16
  if (this.debugRaw) {
package/dist/index.js CHANGED
@@ -8,10 +8,12 @@ export * from './bpd.js';
8
8
  export * from './client.js';
9
9
  export * from './config.js';
10
10
  export * from './dialog.js';
11
+ export * from './electronicStatement.js';
11
12
  export * from './httpClient.js';
12
13
  export * from './message.js';
13
14
  export * from './mt535parser.js';
14
15
  export * from './mt940parser.js';
15
16
  export * from './segment.js';
17
+ export { StatementFormat } from './segments/HKEKA.js';
16
18
  export * from './statement.js';
17
19
  export * from './upd.js';
@@ -30,7 +30,15 @@ export class CreditCardStatementInteraction extends CustomerOrderInteraction {
30
30
  const valueFloatStr = value.replaceAll('.', '').replaceAll(',', '.');
31
31
  return parseFloat(valueFloatStr);
32
32
  }
33
- const dikku = response.findSegment(DIKKU.Id);
33
+ // A response the bank spread over several messages arrives as several DIKKU
34
+ // segments. The balance is the same in each, the transactions are not.
35
+ const dikkuSegments = response.findAllSegments(DIKKU.Id);
36
+ const dikku = dikkuSegments[0]
37
+ ? {
38
+ ...dikkuSegments[0],
39
+ transactions: dikkuSegments.flatMap((segment) => segment.transactions ?? []),
40
+ }
41
+ : undefined;
34
42
  if (dikku) {
35
43
  const creditDebit = dikku.balance.creditDebit;
36
44
  const balanceAmount = dikku.balance.amount.value * (creditDebit === 'D' ? -1 : 1);
@@ -0,0 +1,88 @@
1
+ import { HIEKA } from '../segments/HIEKA.js';
2
+ import { HKEKA } from '../segments/HKEKA.js';
3
+ import { CustomerOrderInteraction } from './customerInteraction.js';
4
+ /**
5
+ * Turns the latin1 string the parser produced back into the bytes the bank sent.
6
+ */
7
+ function toBytes(binary) {
8
+ const bytes = new Uint8Array(binary.length);
9
+ for (let i = 0; i < binary.length; i++) {
10
+ bytes[i] = binary.charCodeAt(i) & 0xff;
11
+ }
12
+ return bytes;
13
+ }
14
+ const PDF_MAGIC = '%PDF';
15
+ /**
16
+ * Some banks base64-encode the document although the field is declared binary — a known
17
+ * quirk of HIEKP v1 that may apply here as well.
18
+ *
19
+ * This only unwraps when it can prove the result: the payload must consist of base64
20
+ * characters only AND decode to something that actually starts with a PDF header.
21
+ * Anything else is passed through untouched, so a document is never silently mangled.
22
+ */
23
+ function unwrapBase64(bytes) {
24
+ const text = new TextDecoder('latin1').decode(bytes);
25
+ if (text.startsWith(PDF_MAGIC) || !/^[A-Za-z0-9+/\s]+={0,2}\s*$/.test(text)) {
26
+ return bytes;
27
+ }
28
+ try {
29
+ const decoded = Buffer.from(text, 'base64');
30
+ return decoded.subarray(0, PDF_MAGIC.length).toString('latin1') === PDF_MAGIC
31
+ ? new Uint8Array(decoded)
32
+ : bytes;
33
+ }
34
+ catch {
35
+ return bytes;
36
+ }
37
+ }
38
+ export class ElectronicStatementInteraction extends CustomerOrderInteraction {
39
+ accountNumber;
40
+ options;
41
+ constructor(accountNumber, options = {}) {
42
+ super(HKEKA.Id, HIEKA.Id);
43
+ this.accountNumber = accountNumber;
44
+ this.options = options;
45
+ }
46
+ createSegments(init) {
47
+ const bankAccount = init.getBankAccount(this.accountNumber);
48
+ const version = init.getMaxSupportedTransactionVersion(HKEKA.Id);
49
+ if (!version) {
50
+ throw Error(`There is no supported version for business transaction '${HKEKA.Id}'`);
51
+ }
52
+ const params = init.getTransactionParameters(HKEKA.Id);
53
+ const format = this.options.format ?? params?.supportedFormats?.[0];
54
+ const hkeka = {
55
+ header: { segId: HKEKA.Id, segNr: 0, version: version },
56
+ account: bankAccount,
57
+ statementFormat: format,
58
+ statementNumber: this.options.number,
59
+ statementYear: this.options.year,
60
+ maxEntries: this.options.maxEntries,
61
+ offset: this.options.offset,
62
+ };
63
+ return [hkeka];
64
+ }
65
+ handleResponse(response, clientResponse) {
66
+ const segments = response.findAllSegments(HIEKA.Id);
67
+ clientResponse.statements = segments.map((hieka) => {
68
+ const names = [hieka.name, hieka.name2, hieka.name3].filter((name) => !!name);
69
+ return {
70
+ format: hieka.format,
71
+ from: hieka.timeRange?.from,
72
+ to: hieka.timeRange?.to,
73
+ date: hieka.date,
74
+ year: hieka.year,
75
+ number: hieka.number,
76
+ document: unwrapBase64(toBytes(hieka.booked ?? '')),
77
+ closingInfo: hieka.closingInfo,
78
+ conditionsInfo: hieka.conditionsInfo,
79
+ advertisement: hieka.advertisement,
80
+ iban: hieka.iban,
81
+ bic: hieka.bic,
82
+ accountName: names.length > 0 ? names.join(' ') : undefined,
83
+ receipt: hieka.receipt,
84
+ };
85
+ });
86
+ clientResponse.nextOffset = clientResponse.bankAnswers.find((answer) => answer.code === 3040)?.params?.[0];
87
+ }
88
+ }
@@ -143,7 +143,9 @@ export class InitDialogInteraction extends CustomerInteraction {
143
143
  const hiupa = response.findSegment(HIUPA.Id);
144
144
  if (hiupa) {
145
145
  const hiupds = response.findAllSegments(HIUPD.Id);
146
- const accounts = hiupds.map((upd) => {
146
+ const accounts = hiupds
147
+ .filter((upd) => upd.account)
148
+ .map((upd) => {
147
149
  return {
148
150
  accountNumber: upd.account.accountNumber,
149
151
  subAccountId: upd.account.subAccountId,
@@ -40,17 +40,23 @@ export class PortfolioInteraction extends CustomerOrderInteraction {
40
40
  return [hkwpd];
41
41
  }
42
42
  handleResponse(response, clientResponse) {
43
- const hiwpdSegment = response.findSegment(HIWPD.Id);
44
- if (hiwpdSegment?.portfolioStatement) {
43
+ // A response the bank spread over several messages arrives as several HIWPD
44
+ // segments carrying one continuous MT535 stream, so their payloads are joined.
45
+ const portfolioStatement = response
46
+ .findAllSegments(HIWPD.Id)
47
+ .map((segment) => segment.portfolioStatement)
48
+ .filter((statement) => !!statement)
49
+ .join('');
50
+ if (portfolioStatement) {
45
51
  try {
46
52
  // Parse the MT535 data
47
- const parser = new Mt535Parser(hiwpdSegment.portfolioStatement);
53
+ const parser = new Mt535Parser(portfolioStatement);
48
54
  clientResponse.portfolioStatement = parser.parse();
49
55
  }
50
56
  catch (error) {
51
57
  console.warn('Failed to parse MT535 portfolio statement:', error);
52
58
  // Fallback: provide raw data if parsing fails
53
- clientResponse.rawMT535Data = hiwpdSegment.portfolioStatement;
59
+ clientResponse.rawMT535Data = portfolioStatement;
54
60
  }
55
61
  }
56
62
  }
@@ -29,9 +29,11 @@ export class SepaAccountInteraction extends CustomerOrderInteraction {
29
29
  return [hkspa];
30
30
  }
31
31
  handleResponse(response, clientResponse) {
32
- const hispa = response.findSegment(HISPA.Id);
33
- if (hispa) {
34
- clientResponse.sepaAccounts = hispa.sepaAccounts || [];
32
+ // A response the bank spread over several messages arrives as several HISPA
33
+ // segments, each carrying its own share of the accounts.
34
+ const hispaSegments = response.findAllSegments(HISPA.Id);
35
+ if (hispaSegments.length > 0) {
36
+ clientResponse.sepaAccounts = hispaSegments.flatMap((segment) => segment.sepaAccounts ?? []);
35
37
  this.dialog?.config.bankingInformation.upd?.bankAccounts.forEach((bankAccount) => {
36
38
  bankAccount.isSepaAccount = false;
37
39
  });
@@ -34,12 +34,17 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction {
34
34
  return [hkcaz];
35
35
  }
36
36
  handleResponse(response, clientResponse) {
37
- const hicaz = response.findSegment(HICAZ.Id);
38
- if (hicaz?.bookedTransactions && hicaz.bookedTransactions.length > 0) {
37
+ // A response the bank spread over several messages arrives as several HICAZ
38
+ // segments, each carrying its own share of the CAMT documents. Taking only the
39
+ // first one would silently drop everything after it.
40
+ const camtMessages = response
41
+ .findAllSegments(HICAZ.Id)
42
+ .flatMap((segment) => segment.bookedTransactions ?? []);
43
+ if (camtMessages.length > 0) {
39
44
  try {
40
45
  // Parse all CAMT messages (one per booking day) and combine statements
41
46
  const allStatements = [];
42
- for (const camtMessage of hicaz.bookedTransactions) {
47
+ for (const camtMessage of camtMessages) {
43
48
  // The regex looks for the XML declaration `<?xml ... ?>`
44
49
  // and checks if it contains the attribute encoding="UTF-8".
45
50
  // The 'i' flag makes the match case-insensitive (e.g., for "utf-8").
@@ -29,10 +29,17 @@ export class StatementInteractionMT940 extends CustomerOrderInteraction {
29
29
  return [hkkaz];
30
30
  }
31
31
  handleResponse(response, clientResponse) {
32
- const hikaz = response.findSegment(HIKAZ.Id);
33
- if (hikaz?.bookedTransactions) {
32
+ // A response the bank spread over several messages arrives as several HIKAZ
33
+ // segments. Unlike CAMT these carry one continuous MT940 stream, so their
34
+ // payloads are joined rather than listed.
35
+ const bookedTransactions = response
36
+ .findAllSegments(HIKAZ.Id)
37
+ .map((segment) => segment.bookedTransactions)
38
+ .filter((booked) => !!booked)
39
+ .join('');
40
+ if (bookedTransactions) {
34
41
  try {
35
- const parser = new Mt940Parser(hikaz.bookedTransactions);
42
+ const parser = new Mt940Parser(bookedTransactions);
36
43
  clientResponse.statements = parser.parse();
37
44
  }
38
45
  catch (error) {
package/dist/message.js CHANGED
@@ -80,7 +80,11 @@ export class Message {
80
80
  return message;
81
81
  }
82
82
  static decodeSegment(text, partedResponseSegId) {
83
- if (partedResponseSegId && text.startsWith(partedResponseSegId)) {
83
+ // The colon matters: a segment starts with `SEGID:number:version`, so a plain
84
+ // `startsWith` would also catch the parameter segment whose id merely begins the
85
+ // same way — HIEKAS when looking for HIEKA, HICAZS for HICAZ. Those would then be
86
+ // held back as PARTED and never decoded.
87
+ if (partedResponseSegId && text.startsWith(`${partedResponseSegId}:`)) {
84
88
  const partedSegment = {
85
89
  header: {
86
90
  ...SegmentDefinition.header.decode(text, 1),
@@ -0,0 +1,45 @@
1
+ import { AlphaNumeric } from '../dataElements/AlphaNumeric.js';
2
+ import { Binary } from '../dataElements/Binary.js';
3
+ import { Dat } from '../dataElements/Dat.js';
4
+ import { Numeric } from '../dataElements/Numeric.js';
5
+ import { DataGroup } from '../dataGroups/DataGroup.js';
6
+ import { SegmentDefinition } from '../segmentDefinition.js';
7
+ /**
8
+ * Electronic account statement response (Elektronischer Kontoauszug)
9
+ *
10
+ * The element order follows the FinTS 3.0 specification, and two details of it are easy
11
+ * to get wrong:
12
+ *
13
+ * - `booked` sits AFTER date/year/number, not before them. HIEKP v2 orders the same
14
+ * fields the other way round — the order is specific to each segment and cannot be
15
+ * carried over from one to the other.
16
+ * - Only version 5 carries date/year/number at all; up to version 4 `booked` follows
17
+ * the time range directly, and version 1 has no iban/bic/name either. Decoding an
18
+ * older response with the version 5 layout does not fail, it silently shifts every
19
+ * field by three positions and hands out the advertisement text as the document.
20
+ */
21
+ export class HIEKA extends SegmentDefinition {
22
+ static Id = 'HIEKA';
23
+ static Version = 5;
24
+ constructor() {
25
+ super(HIEKA.Id);
26
+ }
27
+ version = HIEKA.Version;
28
+ elements = [
29
+ new AlphaNumeric('format', 1, 1, 1),
30
+ new DataGroup('timeRange', [new Dat('from', 0, 1), new Dat('to', 0, 1)], 1, 1),
31
+ new Dat('date', 0, 1, 5),
32
+ new Numeric('year', 0, 1, 4, 5),
33
+ new Numeric('number', 0, 1, 5, 5),
34
+ new Binary('booked', 1, 1),
35
+ new AlphaNumeric('closingInfo', 0, 1, 65536),
36
+ new AlphaNumeric('conditionsInfo', 0, 1, 65536),
37
+ new AlphaNumeric('advertisement', 0, 1, 65536),
38
+ new AlphaNumeric('iban', 0, 1, 34, 2),
39
+ new AlphaNumeric('bic', 0, 1, 11, 2),
40
+ new AlphaNumeric('name', 0, 1, 35, 2),
41
+ new AlphaNumeric('name2', 0, 1, 35, 2),
42
+ new AlphaNumeric('name3', 0, 1, 35, 2),
43
+ new Binary('receipt', 0, 1),
44
+ ];
45
+ }
@@ -0,0 +1,18 @@
1
+ import { AlphaNumeric } from '../dataElements/AlphaNumeric.js';
2
+ import { YesNo } from '../dataElements/YesNo.js';
3
+ import { BusinessTransactionParameter, } from './businessTransactionParameter.js';
4
+ /**
5
+ * Parameters for the HKEKA business transaction (electronic account statements)
6
+ */
7
+ export class HIEKAS extends BusinessTransactionParameter {
8
+ static Id = 'HIEKAS';
9
+ version = 5;
10
+ constructor() {
11
+ super(HIEKAS.Id, [
12
+ new YesNo('indexAllowed', 1, 1),
13
+ new YesNo('receiptRequired', 1, 1),
14
+ new YesNo('maxEntryCountAllowed', 1, 1),
15
+ new AlphaNumeric('supportedFormats', 1, 9, 1),
16
+ ]);
17
+ }
18
+ }
@@ -3,8 +3,8 @@ import { Dat } from '../dataElements/Dat.js';
3
3
  import { Numeric } from '../dataElements/Numeric.js';
4
4
  import { Text } from '../dataElements/Text.js';
5
5
  import { YesNo } from '../dataElements/YesNo.js';
6
- import { CamtAccountGroup } from '../dataGroups/CamtAccount.js';
7
6
  import { DataGroup } from '../dataGroups/DataGroup.js';
7
+ import { InternationalAccountGroup, } from '../dataGroups/InternationalAccount.js';
8
8
  import { SegmentDefinition } from '../segmentDefinition.js';
9
9
  /**
10
10
  * Request account transactions in a given period (CAMT format)
@@ -17,7 +17,7 @@ export class HKCAZ extends SegmentDefinition {
17
17
  }
18
18
  version = HKCAZ.Version;
19
19
  elements = [
20
- new CamtAccountGroup('account', 1, 1),
20
+ new InternationalAccountGroup('account', 1, 1),
21
21
  new DataGroup('acceptedCamtFormats', [new Text('camtFormat', 1, 99)], 1, 1), // Support multiple camt-formats
22
22
  new YesNo('allAccounts', 1, 1),
23
23
  new Dat('from', 0, 1),
@@ -0,0 +1,43 @@
1
+ import { AlphaNumeric } from '../dataElements/AlphaNumeric.js';
2
+ import { Numeric } from '../dataElements/Numeric.js';
3
+ import { AccountGroup } from '../dataGroups/Account.js';
4
+ import { InternationalAccountGroup, } from '../dataGroups/InternationalAccount.js';
5
+ import { SegmentDefinition } from '../segmentDefinition.js';
6
+ /**
7
+ * Format of the electronic account statement.
8
+ * The bank announces the formats it supports in the HIEKAS parameters.
9
+ */
10
+ export var StatementFormat;
11
+ (function (StatementFormat) {
12
+ StatementFormat["MT940"] = "1";
13
+ StatementFormat["ISO8583"] = "2";
14
+ StatementFormat["PDF"] = "3";
15
+ })(StatementFormat || (StatementFormat = {}));
16
+ /**
17
+ * Request an electronic account statement (Elektronischer Kontoauszug)
18
+ *
19
+ * Unlike HKKAZ/HKCAZ this does not return individual transactions but the statement
20
+ * document the bank files in the customer's electronic mailbox — for most banks a PDF.
21
+ *
22
+ * A statement is handed out once: the bank keeps track of which statements have already
23
+ * been fetched and announces remaining ones with answer code 3040 plus an offset. Banks
24
+ * that set `receiptRequired` in their HIEKAS parameters expect each statement to be
25
+ * acknowledged before they consider it delivered.
26
+ */
27
+ export class HKEKA extends SegmentDefinition {
28
+ static Id = 'HKEKA';
29
+ static Version = 5;
30
+ constructor() {
31
+ super(HKEKA.Id);
32
+ }
33
+ version = HKEKA.Version;
34
+ elements = [
35
+ new AccountGroup('account', 1, 1, 1, 3),
36
+ new InternationalAccountGroup('account', 1, 1, 4),
37
+ new AlphaNumeric('statementFormat', 0, 1, 1),
38
+ new Numeric('statementNumber', 0, 1, 5),
39
+ new Numeric('statementYear', 0, 1, 4, 3),
40
+ new Numeric('maxEntries', 0, 1, 4),
41
+ new AlphaNumeric('offset', 0, 1, 35),
42
+ ];
43
+ }
@@ -5,6 +5,8 @@ import { DKKKU } from './DKKKU.js';
5
5
  import { HIBPA } from './HIBPA.js';
6
6
  import { HICAZ } from './HICAZ.js';
7
7
  import { HICAZS } from './HICAZS.js';
8
+ import { HIEKA } from './HIEKA.js';
9
+ import { HIEKAS } from './HIEKAS.js';
8
10
  import { HIKAZ } from './HIKAZ.js';
9
11
  import { HIKAZS } from './HIKAZS.js';
10
12
  import { HIKIM } from './HIKIM.js';
@@ -23,6 +25,7 @@ import { HIUPA } from './HIUPA.js';
23
25
  import { HIUPD } from './HIUPD.js';
24
26
  import { HIWPD } from './HIWPD.js';
25
27
  import { HKCAZ } from './HKCAZ.js';
28
+ import { HKEKA } from './HKEKA.js';
26
29
  import { HKEND } from './HKEND.js';
27
30
  import { HKIDN } from './HKIDN.js';
28
31
  import { HKKAZ } from './HKKAZ.js';
@@ -75,6 +78,9 @@ export function registerSegments() {
75
78
  registerSegmentDefinition(new HKCAZ());
76
79
  registerSegmentDefinition(new HICAZ());
77
80
  registerSegmentDefinition(new HICAZS());
81
+ registerSegmentDefinition(new HKEKA());
82
+ registerSegmentDefinition(new HIEKA());
83
+ registerSegmentDefinition(new HIEKAS());
78
84
  registerSegmentDefinition(new HKWPD());
79
85
  registerSegmentDefinition(new HIWPD());
80
86
  registerSegmentDefinition(new HKWPD());