lib-fints 1.4.8 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +21 -15
  2. package/dist/accountDescriptor.js +65 -0
  3. package/dist/bankAccount.js +8 -0
  4. package/dist/client.js +71 -33
  5. package/dist/config.js +57 -10
  6. package/dist/dataElements/Binary.js +18 -1
  7. package/dist/dialog.js +52 -31
  8. package/dist/electronicStatement.js +1 -0
  9. package/dist/httpClient.js +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/interactions/balanceInteraction.js +10 -8
  12. package/dist/interactions/creditcardStatementInteraction.js +17 -8
  13. package/dist/interactions/electronicStatementInteraction.js +89 -0
  14. package/dist/interactions/portfolioInteraction.js +19 -11
  15. package/dist/interactions/sepaAccountInteraction.js +11 -8
  16. package/dist/interactions/statementInteractionCAMT.js +14 -8
  17. package/dist/interactions/statementInteractionMT940.js +17 -9
  18. package/dist/message.js +10 -2
  19. package/dist/segments/HIEKA.js +45 -0
  20. package/dist/segments/HIEKAS.js +18 -0
  21. package/dist/segments/HKEKA.js +43 -0
  22. package/dist/segments/HKKAZ.js +1 -1
  23. package/dist/segments/registry.js +6 -0
  24. package/dist/tests/HKEKA.test.js +198 -0
  25. package/dist/tests/accountDescriptor.test.js +99 -0
  26. package/dist/tests/accountReference.test.js +126 -0
  27. package/dist/tests/interactionAccountDescriptor.test.js +153 -0
  28. package/dist/tests/message.test.js +18 -0
  29. package/dist/tests/partedResponse.test.js +181 -0
  30. package/dist/types/accountDescriptor.d.ts +49 -0
  31. package/dist/types/accountDescriptor.d.ts.map +1 -0
  32. package/dist/types/bankAccount.d.ts +13 -0
  33. package/dist/types/bankAccount.d.ts.map +1 -1
  34. package/dist/types/client.d.ts +45 -16
  35. package/dist/types/client.d.ts.map +1 -1
  36. package/dist/types/config.d.ts +27 -6
  37. package/dist/types/config.d.ts.map +1 -1
  38. package/dist/types/dataElements/Binary.d.ts +7 -0
  39. package/dist/types/dataElements/Binary.d.ts.map +1 -1
  40. package/dist/types/dialog.d.ts +14 -0
  41. package/dist/types/dialog.d.ts.map +1 -1
  42. package/dist/types/electronicStatement.d.ts +40 -0
  43. package/dist/types/electronicStatement.d.ts.map +1 -0
  44. package/dist/types/index.d.ts +3 -0
  45. package/dist/types/index.d.ts.map +1 -1
  46. package/dist/types/interactions/balanceInteraction.d.ts +3 -2
  47. package/dist/types/interactions/balanceInteraction.d.ts.map +1 -1
  48. package/dist/types/interactions/creditcardStatementInteraction.d.ts +3 -2
  49. package/dist/types/interactions/creditcardStatementInteraction.d.ts.map +1 -1
  50. package/dist/types/interactions/electronicStatementInteraction.d.ts +34 -0
  51. package/dist/types/interactions/electronicStatementInteraction.d.ts.map +1 -0
  52. package/dist/types/interactions/portfolioInteraction.d.ts +3 -2
  53. package/dist/types/interactions/portfolioInteraction.d.ts.map +1 -1
  54. package/dist/types/interactions/sepaAccountInteraction.d.ts +3 -2
  55. package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -1
  56. package/dist/types/interactions/statementInteractionCAMT.d.ts +3 -2
  57. package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -1
  58. package/dist/types/interactions/statementInteractionMT940.d.ts +3 -2
  59. package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -1
  60. package/dist/types/message.d.ts.map +1 -1
  61. package/dist/types/segments/HIEKA.d.ts +52 -0
  62. package/dist/types/segments/HIEKA.d.ts.map +1 -0
  63. package/dist/types/segments/HIEKAS.d.ts +21 -0
  64. package/dist/types/segments/HIEKAS.d.ts.map +1 -0
  65. package/dist/types/segments/HKEKA.d.ts +54 -0
  66. package/dist/types/segments/HKEKA.d.ts.map +1 -0
  67. package/dist/types/segments/HKKAZ.d.ts +2 -2
  68. package/dist/types/segments/HKKAZ.d.ts.map +1 -1
  69. package/dist/types/segments/registry.d.ts.map +1 -1
  70. package/dist/types/tests/HKEKA.test.d.ts +2 -0
  71. package/dist/types/tests/HKEKA.test.d.ts.map +1 -0
  72. package/dist/types/tests/accountDescriptor.test.d.ts +2 -0
  73. package/dist/types/tests/accountDescriptor.test.d.ts.map +1 -0
  74. package/dist/types/tests/accountReference.test.d.ts +2 -0
  75. package/dist/types/tests/accountReference.test.d.ts.map +1 -0
  76. package/dist/types/tests/interactionAccountDescriptor.test.d.ts +2 -0
  77. package/dist/types/tests/interactionAccountDescriptor.test.d.ts.map +1 -0
  78. package/dist/types/tests/partedResponse.test.d.ts +2 -0
  79. package/dist/types/tests/partedResponse.test.d.ts.map +1 -0
  80. package/package.json +2 -2
@@ -1,26 +1,28 @@
1
+ import { internationalAccount, nationalAccount } from '../accountDescriptor.js';
2
+ import { describeAccount } from '../bankAccount.js';
1
3
  import { CreditDebit } from '../codes.js';
2
4
  import { HISAL } from '../segments/HISAL.js';
3
5
  import { HKSAL } from '../segments/HKSAL.js';
4
6
  import { CustomerOrderInteraction } from './customerInteraction.js';
5
7
  export class BalanceInteraction extends CustomerOrderInteraction {
6
- accountNumber;
7
- constructor(accountNumber) {
8
+ account;
9
+ constructor(account) {
8
10
  super(HKSAL.Id, HISAL.Id);
9
- this.accountNumber = accountNumber;
11
+ this.account = account;
10
12
  }
11
13
  createSegments(init) {
12
- const bankAccount = init.getBankAccount(this.accountNumber);
13
- if (!init.isAccountTransactionSupported(this.accountNumber, this.segId)) {
14
- throw Error(`Account ${this.accountNumber} does not support business transaction '${this.segId}'`);
14
+ const bankAccount = init.getBankAccount(this.account);
15
+ if (!init.isAccountTransactionSupported(this.account, this.segId)) {
16
+ throw Error(`Account ${describeAccount(this.account)} does not support business transaction '${this.segId}'`);
15
17
  }
16
18
  const version = init.getMaxSupportedTransactionVersion(HKSAL.Id);
17
19
  if (!version) {
18
20
  throw Error(`There is no supported version for business transaction '${HKSAL.Id}`);
19
21
  }
20
- const account = version <= 6 ? { ...bankAccount, iban: undefined, bic: undefined } : bankAccount;
22
+ const account = version <= 6 ? nationalAccount(bankAccount) : internationalAccount(init, bankAccount);
21
23
  const hksal = {
22
24
  header: { segId: HKSAL.Id, segNr: 0, version: version },
23
- account,
25
+ account: account,
24
26
  allAccounts: false,
25
27
  };
26
28
  return [hksal];
@@ -1,18 +1,19 @@
1
+ import { describeAccount } from '../bankAccount.js';
1
2
  import { DIKKU } from '../segments/DIKKU.js';
2
3
  import { DKKKU } from '../segments/DKKKU.js';
3
4
  import { CustomerOrderInteraction } from './customerInteraction.js';
4
5
  export class CreditCardStatementInteraction extends CustomerOrderInteraction {
5
- accountNumber;
6
+ account;
6
7
  from;
7
- constructor(accountNumber, from) {
8
+ constructor(account, from) {
8
9
  super(DKKKU.Id, DIKKU.Id);
9
- this.accountNumber = accountNumber;
10
+ this.account = account;
10
11
  this.from = from;
11
12
  }
12
13
  createSegments(init) {
13
- const bankAccount = init.getBankAccount(this.accountNumber);
14
- if (!init.isAccountTransactionSupported(this.accountNumber, this.segId)) {
15
- throw Error(`Account ${this.accountNumber} does not support business transaction '${this.segId}'`);
14
+ const bankAccount = init.getBankAccount(this.account);
15
+ if (!init.isAccountTransactionSupported(this.account, this.segId)) {
16
+ throw Error(`Account ${describeAccount(this.account)} does not support business transaction '${this.segId}'`);
16
17
  }
17
18
  const account = { ...bankAccount, iban: undefined };
18
19
  const version = 2;
@@ -30,7 +31,15 @@ export class CreditCardStatementInteraction extends CustomerOrderInteraction {
30
31
  const valueFloatStr = value.replaceAll('.', '').replaceAll(',', '.');
31
32
  return parseFloat(valueFloatStr);
32
33
  }
33
- const dikku = response.findSegment(DIKKU.Id);
34
+ // A response the bank spread over several messages arrives as several DIKKU
35
+ // segments. The balance is the same in each, the transactions are not.
36
+ const dikkuSegments = response.findAllSegments(DIKKU.Id);
37
+ const dikku = dikkuSegments[0]
38
+ ? {
39
+ ...dikkuSegments[0],
40
+ transactions: dikkuSegments.flatMap((segment) => segment.transactions ?? []),
41
+ }
42
+ : undefined;
34
43
  if (dikku) {
35
44
  const creditDebit = dikku.balance.creditDebit;
36
45
  const balanceAmount = dikku.balance.amount.value * (creditDebit === 'D' ? -1 : 1);
@@ -50,7 +59,7 @@ export class CreditCardStatementInteraction extends CustomerOrderInteraction {
50
59
  if (dikku.transactions) {
51
60
  for (let i = 0; i < dikku.transactions.length; i++) {
52
61
  const parts = dikku.transactions[i].split(':');
53
- // const accountNumber = parts[0];
62
+ // const account = parts[0];
54
63
  const transactionDateStr = parts[1];
55
64
  const valueDateStr = parts[2];
56
65
  const currencyOrig = parts[5];
@@ -0,0 +1,89 @@
1
+ import { internationalAccount, nationalAccount } from '../accountDescriptor.js';
2
+ import { HIEKA } from '../segments/HIEKA.js';
3
+ import { HKEKA } from '../segments/HKEKA.js';
4
+ import { CustomerOrderInteraction } from './customerInteraction.js';
5
+ /**
6
+ * Turns the latin1 string the parser produced back into the bytes the bank sent.
7
+ */
8
+ function toBytes(binary) {
9
+ const bytes = new Uint8Array(binary.length);
10
+ for (let i = 0; i < binary.length; i++) {
11
+ bytes[i] = binary.charCodeAt(i) & 0xff;
12
+ }
13
+ return bytes;
14
+ }
15
+ const PDF_MAGIC = '%PDF';
16
+ /**
17
+ * Some banks base64-encode the document although the field is declared binary — a known
18
+ * quirk of HIEKP v1 that may apply here as well.
19
+ *
20
+ * This only unwraps when it can prove the result: the payload must consist of base64
21
+ * characters only AND decode to something that actually starts with a PDF header.
22
+ * Anything else is passed through untouched, so a document is never silently mangled.
23
+ */
24
+ function unwrapBase64(bytes) {
25
+ const text = new TextDecoder('latin1').decode(bytes);
26
+ if (text.startsWith(PDF_MAGIC) || !/^[A-Za-z0-9+/\s]+={0,2}\s*$/.test(text)) {
27
+ return bytes;
28
+ }
29
+ try {
30
+ const decoded = Buffer.from(text, 'base64');
31
+ return decoded.subarray(0, PDF_MAGIC.length).toString('latin1') === PDF_MAGIC
32
+ ? new Uint8Array(decoded)
33
+ : bytes;
34
+ }
35
+ catch {
36
+ return bytes;
37
+ }
38
+ }
39
+ export class ElectronicStatementInteraction extends CustomerOrderInteraction {
40
+ account;
41
+ options;
42
+ constructor(account, options = {}) {
43
+ super(HKEKA.Id, HIEKA.Id);
44
+ this.account = account;
45
+ this.options = options;
46
+ }
47
+ createSegments(init) {
48
+ const bankAccount = init.getBankAccount(this.account);
49
+ const version = init.getMaxSupportedTransactionVersion(HKEKA.Id);
50
+ if (!version) {
51
+ throw Error(`There is no supported version for business transaction '${HKEKA.Id}'`);
52
+ }
53
+ const params = init.getTransactionParameters(HKEKA.Id);
54
+ const format = this.options.format ?? params?.supportedFormats?.[0];
55
+ const hkeka = {
56
+ header: { segId: HKEKA.Id, segNr: 0, version: version },
57
+ account: version <= 3 ? nationalAccount(bankAccount) : internationalAccount(init, bankAccount),
58
+ statementFormat: format,
59
+ statementNumber: this.options.number,
60
+ statementYear: this.options.year,
61
+ maxEntries: this.options.maxEntries,
62
+ offset: this.options.offset,
63
+ };
64
+ return [hkeka];
65
+ }
66
+ handleResponse(response, clientResponse) {
67
+ const segments = response.findAllSegments(HIEKA.Id);
68
+ clientResponse.statements = segments.map((hieka) => {
69
+ const names = [hieka.name, hieka.name2, hieka.name3].filter((name) => !!name);
70
+ return {
71
+ format: hieka.format,
72
+ from: hieka.timeRange?.from,
73
+ to: hieka.timeRange?.to,
74
+ date: hieka.date,
75
+ year: hieka.year,
76
+ number: hieka.number,
77
+ document: unwrapBase64(toBytes(hieka.booked ?? '')),
78
+ closingInfo: hieka.closingInfo,
79
+ conditionsInfo: hieka.conditionsInfo,
80
+ advertisement: hieka.advertisement,
81
+ iban: hieka.iban,
82
+ bic: hieka.bic,
83
+ accountName: names.length > 0 ? names.join(' ') : undefined,
84
+ receipt: hieka.receipt,
85
+ };
86
+ });
87
+ clientResponse.nextOffset = clientResponse.bankAnswers.find((answer) => answer.code === 3040)?.params?.[0];
88
+ }
89
+ }
@@ -1,3 +1,5 @@
1
+ import { nationalAccount } from '../accountDescriptor.js';
2
+ import { describeAccount } from '../bankAccount.js';
1
3
  import { Mt535Parser } from '../mt535parser.js';
2
4
  import { HIWPD } from '../segments/HIWPD.js';
3
5
  import { HKWPD } from '../segments/HKWPD.js';
@@ -6,25 +8,25 @@ import { CustomerOrderInteraction } from './customerInteraction.js';
6
8
  * Interaction for requesting and parsing stock portfolio information (HKWPD/HIWPD)
7
9
  */
8
10
  export class PortfolioInteraction extends CustomerOrderInteraction {
9
- accountNumber;
11
+ account;
10
12
  currency;
11
13
  priceQuality;
12
14
  maxEntries;
13
15
  paginationMarker;
14
- constructor(accountNumber, currency, priceQuality, maxEntries, paginationMarker) {
16
+ constructor(account, currency, priceQuality, maxEntries, paginationMarker) {
15
17
  super(HKWPD.Id, HIWPD.Id);
16
- this.accountNumber = accountNumber;
18
+ this.account = account;
17
19
  this.currency = currency;
18
20
  this.priceQuality = priceQuality;
19
21
  this.maxEntries = maxEntries;
20
22
  this.paginationMarker = paginationMarker;
21
23
  }
22
24
  createSegments(config) {
23
- const bankAccount = config.getBankAccount(this.accountNumber);
24
- if (!config.isAccountTransactionSupported(this.accountNumber, this.segId)) {
25
- throw Error(`Account ${this.accountNumber} does not support business transaction '${this.segId}'`);
25
+ const bankAccount = config.getBankAccount(this.account);
26
+ if (!config.isAccountTransactionSupported(this.account, this.segId)) {
27
+ throw Error(`Account ${describeAccount(this.account)} does not support business transaction '${this.segId}'`);
26
28
  }
27
- const depotAccount = { ...bankAccount, iban: undefined }; // HKWPD uses KTV which doesn't have IBAN
29
+ const depotAccount = nationalAccount(bankAccount); // HKWPD uses KTV, which has no IBAN
28
30
  const version = config.getMaxSupportedTransactionVersion(HKWPD.Id);
29
31
  if (!version) {
30
32
  throw Error(`There is no supported version for business transaction '${HKWPD.Id}'`);
@@ -40,17 +42,23 @@ export class PortfolioInteraction extends CustomerOrderInteraction {
40
42
  return [hkwpd];
41
43
  }
42
44
  handleResponse(response, clientResponse) {
43
- const hiwpdSegment = response.findSegment(HIWPD.Id);
44
- if (hiwpdSegment?.portfolioStatement) {
45
+ // A response the bank spread over several messages arrives as several HIWPD
46
+ // segments carrying one continuous MT535 stream, so their payloads are joined.
47
+ const portfolioStatement = response
48
+ .findAllSegments(HIWPD.Id)
49
+ .map((segment) => segment.portfolioStatement)
50
+ .filter((statement) => !!statement)
51
+ .join('');
52
+ if (portfolioStatement) {
45
53
  try {
46
54
  // Parse the MT535 data
47
- const parser = new Mt535Parser(hiwpdSegment.portfolioStatement);
55
+ const parser = new Mt535Parser(portfolioStatement);
48
56
  clientResponse.portfolioStatement = parser.parse();
49
57
  }
50
58
  catch (error) {
51
59
  console.warn('Failed to parse MT535 portfolio statement:', error);
52
60
  // Fallback: provide raw data if parsing fails
53
- clientResponse.rawMT535Data = hiwpdSegment.portfolioStatement;
61
+ clientResponse.rawMT535Data = portfolioStatement;
54
62
  }
55
63
  }
56
64
  }
@@ -4,7 +4,7 @@ import { CustomerOrderInteraction } from './customerInteraction.js';
4
4
  export class SepaAccountInteraction extends CustomerOrderInteraction {
5
5
  accounts;
6
6
  maxEntries;
7
- constructor(accounts, // optional specific account numbers
7
+ constructor(accounts, // optional: only these accounts
8
8
  maxEntries) {
9
9
  super(HKSPA.Id, HISPA.Id);
10
10
  this.accounts = accounts;
@@ -18,9 +18,7 @@ export class SepaAccountInteraction extends CustomerOrderInteraction {
18
18
  if (!version) {
19
19
  throw Error(`There is no supported version for business transaction '${HKSPA.Id}'`);
20
20
  }
21
- const accounts = this.accounts?.map((accountNumber) => {
22
- return init.getBankAccount(accountNumber);
23
- });
21
+ const accounts = this.accounts?.map((account) => init.getBankAccount(account));
24
22
  const hkspa = {
25
23
  header: { segId: HKSPA.Id, segNr: 0, version: version },
26
24
  accounts: accounts,
@@ -29,14 +27,19 @@ export class SepaAccountInteraction extends CustomerOrderInteraction {
29
27
  return [hkspa];
30
28
  }
31
29
  handleResponse(response, clientResponse) {
32
- const hispa = response.findSegment(HISPA.Id);
33
- if (hispa) {
34
- clientResponse.sepaAccounts = hispa.sepaAccounts || [];
30
+ // A response the bank spread over several messages arrives as several HISPA
31
+ // segments, each carrying its own share of the accounts.
32
+ const hispaSegments = response.findAllSegments(HISPA.Id);
33
+ if (hispaSegments.length > 0) {
34
+ clientResponse.sepaAccounts = hispaSegments.flatMap((segment) => segment.sepaAccounts ?? []);
35
35
  this.dialog?.config.bankingInformation.upd?.bankAccounts.forEach((bankAccount) => {
36
36
  bankAccount.isSepaAccount = false;
37
37
  });
38
38
  clientResponse.sepaAccounts.forEach((sepaAccount) => {
39
- const bankAccount = this.dialog?.config.getBankAccount(sepaAccount.accountNumber);
39
+ // Matched, not resolved: this is the bank listing its own accounts, and at an
40
+ // institution where two of them share a number, demanding an unambiguous
41
+ // answer here would fail every dialog before it reached its order.
42
+ const bankAccount = this.dialog?.config.matchBankAccount(sepaAccount);
40
43
  if (bankAccount && !bankAccount.isSepaAccount) {
41
44
  bankAccount.isSepaAccount = sepaAccount.isSepaAccount;
42
45
  bankAccount.iban = sepaAccount.iban;
@@ -1,19 +1,20 @@
1
+ import { internationalAccount } from '../accountDescriptor.js';
1
2
  import { CamtParser } from '../camtParser.js';
2
3
  import { HICAZ } from '../segments/HICAZ.js';
3
4
  import { HKCAZ } from '../segments/HKCAZ.js';
4
5
  import { CustomerOrderInteraction } from './customerInteraction.js';
5
6
  export class StatementInteractionCAMT extends CustomerOrderInteraction {
6
- accountNumber;
7
+ account;
7
8
  from;
8
9
  to;
9
- constructor(accountNumber, from, to) {
10
+ constructor(account, from, to) {
10
11
  super(HKCAZ.Id, HICAZ.Id);
11
- this.accountNumber = accountNumber;
12
+ this.account = account;
12
13
  this.from = from;
13
14
  this.to = to;
14
15
  }
15
16
  createSegments(init) {
16
- const bankAccount = init.getBankAccount(this.accountNumber);
17
+ const bankAccount = init.getBankAccount(this.account);
17
18
  const version = init.getMaxSupportedTransactionVersion(HKCAZ.Id);
18
19
  if (!version) {
19
20
  throw Error(`There is no supported version for business transaction '${HKCAZ.Id}'`);
@@ -25,7 +26,7 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction {
25
26
  }
26
27
  const hkcaz = {
27
28
  header: { segId: HKCAZ.Id, segNr: 0, version: version },
28
- account: bankAccount,
29
+ account: internationalAccount(init, bankAccount),
29
30
  acceptedCamtFormats: acceptedCamtFormats,
30
31
  allAccounts: false,
31
32
  from: this.from,
@@ -34,12 +35,17 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction {
34
35
  return [hkcaz];
35
36
  }
36
37
  handleResponse(response, clientResponse) {
37
- const hicaz = response.findSegment(HICAZ.Id);
38
- if (hicaz?.bookedTransactions && hicaz.bookedTransactions.length > 0) {
38
+ // A response the bank spread over several messages arrives as several HICAZ
39
+ // segments, each carrying its own share of the CAMT documents. Taking only the
40
+ // first one would silently drop everything after it.
41
+ const camtMessages = response
42
+ .findAllSegments(HICAZ.Id)
43
+ .flatMap((segment) => segment.bookedTransactions ?? []);
44
+ if (camtMessages.length > 0) {
39
45
  try {
40
46
  // Parse all CAMT messages (one per booking day) and combine statements
41
47
  const allStatements = [];
42
- for (const camtMessage of hicaz.bookedTransactions) {
48
+ for (const camtMessage of camtMessages) {
43
49
  // The regex looks for the XML declaration `<?xml ... ?>`
44
50
  // and checks if it contains the attribute encoding="UTF-8".
45
51
  // The 'i' flag makes the match case-insensitive (e.g., for "utf-8").
@@ -1,27 +1,28 @@
1
+ import { internationalAccount, nationalAccount } from '../accountDescriptor.js';
1
2
  import { Mt940Parser } from '../mt940parser.js';
2
3
  import { HIKAZ } from '../segments/HIKAZ.js';
3
4
  import { HKKAZ } from '../segments/HKKAZ.js';
4
5
  import { CustomerOrderInteraction } from './customerInteraction.js';
5
6
  export class StatementInteractionMT940 extends CustomerOrderInteraction {
6
- accountNumber;
7
+ account;
7
8
  from;
8
9
  to;
9
- constructor(accountNumber, from, to) {
10
+ constructor(account, from, to) {
10
11
  super(HKKAZ.Id, HIKAZ.Id);
11
- this.accountNumber = accountNumber;
12
+ this.account = account;
12
13
  this.from = from;
13
14
  this.to = to;
14
15
  }
15
16
  createSegments(init) {
16
- const bankAccount = init.getBankAccount(this.accountNumber);
17
- const account = { ...bankAccount, iban: undefined };
17
+ const bankAccount = init.getBankAccount(this.account);
18
18
  const version = init.getMaxSupportedTransactionVersion(HKKAZ.Id);
19
19
  if (!version) {
20
20
  throw Error(`There is no supported version for business transaction '${HKKAZ.Id}'`);
21
21
  }
22
+ const account = version <= 6 ? nationalAccount(bankAccount) : internationalAccount(init, bankAccount);
22
23
  const hkkaz = {
23
24
  header: { segId: HKKAZ.Id, segNr: 0, version: version },
24
- account,
25
+ account: account,
25
26
  allAccounts: false,
26
27
  from: this.from,
27
28
  to: this.to,
@@ -29,10 +30,17 @@ export class StatementInteractionMT940 extends CustomerOrderInteraction {
29
30
  return [hkkaz];
30
31
  }
31
32
  handleResponse(response, clientResponse) {
32
- const hikaz = response.findSegment(HIKAZ.Id);
33
- if (hikaz?.bookedTransactions) {
33
+ // A response the bank spread over several messages arrives as several HIKAZ
34
+ // segments. Unlike CAMT these carry one continuous MT940 stream, so their
35
+ // payloads are joined rather than listed.
36
+ const bookedTransactions = response
37
+ .findAllSegments(HIKAZ.Id)
38
+ .map((segment) => segment.bookedTransactions)
39
+ .filter((booked) => !!booked)
40
+ .join('');
41
+ if (bookedTransactions) {
34
42
  try {
35
- const parser = new Mt940Parser(hikaz.bookedTransactions);
43
+ const parser = new Mt940Parser(bookedTransactions);
36
44
  clientResponse.statements = parser.parse();
37
45
  }
38
46
  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),
@@ -139,9 +143,13 @@ export class CustomerMessage extends Message {
139
143
  throw new Error('no signature segment found in message');
140
144
  }
141
145
  const now = new Date();
146
+ // The encryption header carries the same security profile as the
147
+ // signature: PIN version 2 for a two-step TAN method, 1 for one-step.
148
+ // Some banks (Consorsbank, BLZ 76030080) reject a two-step dialog whose
149
+ // HNVSK still says PIN:1 with "9010 Ungültiger Signaturaufbau".
142
150
  const hnvsk = {
143
151
  header: { segId: HNVSK.Id, segNr: 998, version: HNVSK.Version },
144
- secProfile: { secMethod: 'PIN', secVersion: 1 },
152
+ secProfile: { secMethod: 'PIN', secVersion: firstSignature.secProfile.secVersion },
145
153
  secFunc: 998,
146
154
  secRole: 1,
147
155
  secId: { partyType: 1, partyId: firstSignature.secId.partyId },
@@ -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
+ }
@@ -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
+ }
@@ -3,7 +3,7 @@ import { Dat } from '../dataElements/Dat.js';
3
3
  import { Numeric } from '../dataElements/Numeric.js';
4
4
  import { YesNo } from '../dataElements/YesNo.js';
5
5
  import { AccountGroup } from '../dataGroups/Account.js';
6
- import { InternationalAccountGroup } from '../dataGroups/InternationalAccount.js';
6
+ import { InternationalAccountGroup, } from '../dataGroups/InternationalAccount.js';
7
7
  import { SegmentDefinition } from '../segmentDefinition.js';
8
8
  /**
9
9
  * Request account transactions in a given period
@@ -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());