lib-fints 1.4.8 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -15
- package/dist/accountDescriptor.js +65 -0
- package/dist/bankAccount.js +8 -0
- package/dist/client.js +71 -33
- package/dist/config.js +57 -10
- package/dist/dataElements/Binary.js +18 -1
- package/dist/dialog.js +52 -31
- package/dist/electronicStatement.js +1 -0
- package/dist/httpClient.js +1 -1
- package/dist/index.js +2 -0
- package/dist/interactions/balanceInteraction.js +10 -8
- package/dist/interactions/creditcardStatementInteraction.js +17 -8
- package/dist/interactions/electronicStatementInteraction.js +89 -0
- package/dist/interactions/portfolioInteraction.js +19 -11
- package/dist/interactions/sepaAccountInteraction.js +11 -8
- package/dist/interactions/statementInteractionCAMT.js +14 -8
- package/dist/interactions/statementInteractionMT940.js +17 -9
- package/dist/message.js +10 -2
- package/dist/segments/HIEKA.js +45 -0
- package/dist/segments/HIEKAS.js +18 -0
- package/dist/segments/HKEKA.js +43 -0
- package/dist/segments/HKKAZ.js +1 -1
- package/dist/segments/registry.js +6 -0
- package/dist/tests/HKEKA.test.js +198 -0
- package/dist/tests/accountDescriptor.test.js +99 -0
- package/dist/tests/accountReference.test.js +126 -0
- package/dist/tests/interactionAccountDescriptor.test.js +153 -0
- package/dist/tests/message.test.js +18 -0
- package/dist/tests/partedResponse.test.js +181 -0
- package/dist/types/accountDescriptor.d.ts +49 -0
- package/dist/types/accountDescriptor.d.ts.map +1 -0
- package/dist/types/bankAccount.d.ts +13 -0
- package/dist/types/bankAccount.d.ts.map +1 -1
- package/dist/types/client.d.ts +45 -16
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/config.d.ts +27 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/dataElements/Binary.d.ts +7 -0
- package/dist/types/dataElements/Binary.d.ts.map +1 -1
- package/dist/types/dialog.d.ts +14 -0
- package/dist/types/dialog.d.ts.map +1 -1
- package/dist/types/electronicStatement.d.ts +40 -0
- package/dist/types/electronicStatement.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/interactions/balanceInteraction.d.ts +3 -2
- package/dist/types/interactions/balanceInteraction.d.ts.map +1 -1
- package/dist/types/interactions/creditcardStatementInteraction.d.ts +3 -2
- package/dist/types/interactions/creditcardStatementInteraction.d.ts.map +1 -1
- package/dist/types/interactions/electronicStatementInteraction.d.ts +34 -0
- package/dist/types/interactions/electronicStatementInteraction.d.ts.map +1 -0
- package/dist/types/interactions/portfolioInteraction.d.ts +3 -2
- package/dist/types/interactions/portfolioInteraction.d.ts.map +1 -1
- package/dist/types/interactions/sepaAccountInteraction.d.ts +3 -2
- package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -1
- package/dist/types/interactions/statementInteractionCAMT.d.ts +3 -2
- package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -1
- package/dist/types/interactions/statementInteractionMT940.d.ts +3 -2
- package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/segments/HIEKA.d.ts +52 -0
- package/dist/types/segments/HIEKA.d.ts.map +1 -0
- package/dist/types/segments/HIEKAS.d.ts +21 -0
- package/dist/types/segments/HIEKAS.d.ts.map +1 -0
- package/dist/types/segments/HKEKA.d.ts +54 -0
- package/dist/types/segments/HKEKA.d.ts.map +1 -0
- package/dist/types/segments/HKKAZ.d.ts +2 -2
- package/dist/types/segments/HKKAZ.d.ts.map +1 -1
- package/dist/types/segments/registry.d.ts.map +1 -1
- package/dist/types/tests/HKEKA.test.d.ts +2 -0
- package/dist/types/tests/HKEKA.test.d.ts.map +1 -0
- package/dist/types/tests/accountDescriptor.test.d.ts +2 -0
- package/dist/types/tests/accountDescriptor.test.d.ts.map +1 -0
- package/dist/types/tests/accountReference.test.d.ts +2 -0
- package/dist/types/tests/accountReference.test.d.ts.map +1 -0
- package/dist/types/tests/interactionAccountDescriptor.test.d.ts +2 -0
- package/dist/types/tests/interactionAccountDescriptor.test.d.ts.map +1 -0
- package/dist/types/tests/partedResponse.test.d.ts +2 -0
- package/dist/types/tests/partedResponse.test.d.ts.map +1 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
7
|
-
constructor(
|
|
8
|
+
account;
|
|
9
|
+
constructor(account) {
|
|
8
10
|
super(HKSAL.Id, HISAL.Id);
|
|
9
|
-
this.
|
|
11
|
+
this.account = account;
|
|
10
12
|
}
|
|
11
13
|
createSegments(init) {
|
|
12
|
-
const bankAccount = init.getBankAccount(this.
|
|
13
|
-
if (!init.isAccountTransactionSupported(this.
|
|
14
|
-
throw Error(`Account ${this.
|
|
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 ?
|
|
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
|
-
|
|
6
|
+
account;
|
|
6
7
|
from;
|
|
7
|
-
constructor(
|
|
8
|
+
constructor(account, from) {
|
|
8
9
|
super(DKKKU.Id, DIKKU.Id);
|
|
9
|
-
this.
|
|
10
|
+
this.account = account;
|
|
10
11
|
this.from = from;
|
|
11
12
|
}
|
|
12
13
|
createSegments(init) {
|
|
13
|
-
const bankAccount = init.getBankAccount(this.
|
|
14
|
-
if (!init.isAccountTransactionSupported(this.
|
|
15
|
-
throw Error(`Account ${this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
11
|
+
account;
|
|
10
12
|
currency;
|
|
11
13
|
priceQuality;
|
|
12
14
|
maxEntries;
|
|
13
15
|
paginationMarker;
|
|
14
|
-
constructor(
|
|
16
|
+
constructor(account, currency, priceQuality, maxEntries, paginationMarker) {
|
|
15
17
|
super(HKWPD.Id, HIWPD.Id);
|
|
16
|
-
this.
|
|
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.
|
|
24
|
-
if (!config.isAccountTransactionSupported(this.
|
|
25
|
-
throw Error(`Account ${this.
|
|
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 =
|
|
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
|
-
|
|
44
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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((
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
account;
|
|
7
8
|
from;
|
|
8
9
|
to;
|
|
9
|
-
constructor(
|
|
10
|
+
constructor(account, from, to) {
|
|
10
11
|
super(HKCAZ.Id, HICAZ.Id);
|
|
11
|
-
this.
|
|
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.
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
7
|
+
account;
|
|
7
8
|
from;
|
|
8
9
|
to;
|
|
9
|
-
constructor(
|
|
10
|
+
constructor(account, from, to) {
|
|
10
11
|
super(HKKAZ.Id, HIKAZ.Id);
|
|
11
|
-
this.
|
|
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.
|
|
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
|
-
|
|
33
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|
package/dist/segments/HKKAZ.js
CHANGED
|
@@ -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());
|