lib-fints 1.4.8 → 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.
- package/README.md +2 -0
- package/dist/client.js +37 -0
- 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/creditcardStatementInteraction.js +9 -1
- package/dist/interactions/electronicStatementInteraction.js +88 -0
- package/dist/interactions/portfolioInteraction.js +10 -4
- package/dist/interactions/sepaAccountInteraction.js +5 -3
- package/dist/interactions/statementInteractionCAMT.js +8 -3
- package/dist/interactions/statementInteractionMT940.js +10 -3
- package/dist/message.js +5 -1
- package/dist/segments/HIEKA.js +45 -0
- package/dist/segments/HIEKAS.js +18 -0
- package/dist/segments/HKEKA.js +43 -0
- package/dist/segments/registry.js +6 -0
- package/dist/tests/HKEKA.test.js +198 -0
- package/dist/tests/partedResponse.test.js +181 -0
- package/dist/types/client.d.ts +28 -0
- package/dist/types/client.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/creditcardStatementInteraction.d.ts.map +1 -1
- package/dist/types/interactions/electronicStatementInteraction.d.ts +33 -0
- package/dist/types/interactions/electronicStatementInteraction.d.ts.map +1 -0
- package/dist/types/interactions/portfolioInteraction.d.ts.map +1 -1
- package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -1
- package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -1
- 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/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/partedResponse.test.d.ts +2 -0
- package/dist/types/tests/partedResponse.test.d.ts.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -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
|
-
|
|
25
|
+
if (text[0] !== '@') {
|
|
26
|
+
// Not length-prefixed — nothing to go by, take it as it is.
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
const lengthEnd = text.indexOf('@', 1);
|
|
30
|
+
if (lengthEnd < 0) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
const dataStart = lengthEnd + 1;
|
|
34
|
+
const length = Number.parseInt(text.slice(1, lengthEnd), 10);
|
|
35
|
+
return Number.isNaN(length) ? text.slice(dataStart) : text.slice(dataStart, dataStart + length);
|
|
19
36
|
}
|
|
20
37
|
}
|
package/dist/dialog.js
CHANGED
|
@@ -180,39 +180,60 @@ export class Dialog {
|
|
|
180
180
|
}
|
|
181
181
|
return this.config.tanMediaName;
|
|
182
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Collects a response that the bank spreads over several messages.
|
|
185
|
+
*
|
|
186
|
+
* When the bank cannot fit a response into one message it answers with code 3040 plus
|
|
187
|
+
* a continuation mark. Repeating the order with that mark yields the next portion —
|
|
188
|
+
* as a COMPLETE, self-contained response segment, not as a byte-wise continuation of
|
|
189
|
+
* the previous one. A HICAZ follow-up, for example, repeats the account and the CAMT
|
|
190
|
+
* descriptor before carrying its own share of the statements.
|
|
191
|
+
*
|
|
192
|
+
* Every portion is therefore decoded on its own and all of them are placed into the
|
|
193
|
+
* response message the caller holds. Combining their payloads needs to know what the
|
|
194
|
+
* payload means — one MT940 stream continues, a list of CAMT documents is appended —
|
|
195
|
+
* so that step belongs to the interaction, which does it via `findAllSegments`.
|
|
196
|
+
*/
|
|
183
197
|
async handlePartedMessages(message, responseMessage, interaction) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
throw new Error(`Response contains segment with further information, but corresponding segment could not be found or is not specified`);
|
|
191
|
-
}
|
|
192
|
-
const answer = answers.find((a) => a.code === 3040);
|
|
193
|
-
if (!answer || !answer.params || answer.params.length === 0) {
|
|
194
|
-
throw new Error('Expected bank answer to contain continuation mark parameters (code 3040)');
|
|
195
|
-
}
|
|
196
|
-
segmentWithContinuation.continuationMark = answer.params[0];
|
|
197
|
-
const hnhbkSegment = message.findSegment(HNHBK.Id);
|
|
198
|
-
if (!hnhbkSegment) {
|
|
199
|
-
throw new Error('HNHBK segment not found in message');
|
|
200
|
-
}
|
|
201
|
-
hnhbkSegment.msgNr = ++this.lastMessageNumber;
|
|
202
|
-
const nextResponseMessage = await this.httpClient.sendMessage(message);
|
|
203
|
-
const nextPartedSegment = nextResponseMessage.findSegment(PARTED.Id);
|
|
204
|
-
if (nextPartedSegment) {
|
|
205
|
-
nextPartedSegment.rawData =
|
|
206
|
-
partedSegment.rawData +
|
|
207
|
-
nextPartedSegment.rawData.slice(nextPartedSegment.rawData.indexOf('+') + 1);
|
|
208
|
-
partedSegment = nextPartedSegment;
|
|
209
|
-
}
|
|
210
|
-
responseMessage = nextResponseMessage;
|
|
211
|
-
}
|
|
212
|
-
const completeSegment = decode(partedSegment.rawData);
|
|
213
|
-
const index = responseMessage.segments.indexOf(partedSegment);
|
|
214
|
-
responseMessage.segments.splice(index, 1, completeSegment);
|
|
198
|
+
// ALL of them, not just the first: one bank message may well carry several
|
|
199
|
+
// response segments. Taking only the first left the rest sitting in the tree as
|
|
200
|
+
// PARTED, where `findAllSegments` cannot see them — lost without a trace.
|
|
201
|
+
const partedSegments = responseMessage.findAllSegments(PARTED.Id);
|
|
202
|
+
if (partedSegments.length === 0) {
|
|
203
|
+
return;
|
|
215
204
|
}
|
|
205
|
+
// The message the caller holds — every portion has to end up in THIS one, not in
|
|
206
|
+
// the last one we happen to receive.
|
|
207
|
+
const callersMessage = responseMessage;
|
|
208
|
+
const rawPortions = partedSegments.map((segment) => segment.rawData);
|
|
209
|
+
while (responseMessage.hasReturnCode(3040)) {
|
|
210
|
+
const answers = responseMessage.getBankAnswers();
|
|
211
|
+
const segmentWithContinuation = message.segments.find((s) => s.header.segId === interaction.segId);
|
|
212
|
+
if (!segmentWithContinuation) {
|
|
213
|
+
throw new Error(`Response contains segment with further information, but corresponding segment could not be found or is not specified`);
|
|
214
|
+
}
|
|
215
|
+
const answer = answers.find((a) => a.code === 3040);
|
|
216
|
+
if (!answer || !answer.params || answer.params.length === 0) {
|
|
217
|
+
throw new Error('Expected bank answer to contain continuation mark parameters (code 3040)');
|
|
218
|
+
}
|
|
219
|
+
segmentWithContinuation.continuationMark = answer.params[0];
|
|
220
|
+
const hnhbkSegment = message.findSegment(HNHBK.Id);
|
|
221
|
+
if (!hnhbkSegment) {
|
|
222
|
+
throw new Error('HNHBK segment not found in message');
|
|
223
|
+
}
|
|
224
|
+
hnhbkSegment.msgNr = ++this.lastMessageNumber;
|
|
225
|
+
const nextResponseMessage = await this.httpClient.sendMessage(message);
|
|
226
|
+
rawPortions.push(...nextResponseMessage
|
|
227
|
+
.findAllSegments(PARTED.Id)
|
|
228
|
+
.map((segment) => segment.rawData));
|
|
229
|
+
responseMessage = nextResponseMessage;
|
|
230
|
+
}
|
|
231
|
+
// Every PARTED placeholder gives way to the decoded portions, at the position of
|
|
232
|
+
// the first one so the segment order stays intact.
|
|
233
|
+
const index = callersMessage.segments.indexOf(partedSegments[0]);
|
|
234
|
+
const withoutPlaceholders = callersMessage.segments.filter((segment) => segment.header.segId !== PARTED.Id);
|
|
235
|
+
withoutPlaceholders.splice(index, 0, ...rawPortions.map((raw) => decode(raw)));
|
|
236
|
+
callersMessage.segments = withoutPlaceholders;
|
|
216
237
|
}
|
|
217
238
|
checkEnded(response) {
|
|
218
239
|
if (response.bankAnswers.some((answer) => answer.code === 100) ||
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/httpClient.js
CHANGED
|
@@ -10,7 +10,7 @@ export class HttpClient {
|
|
|
10
10
|
}
|
|
11
11
|
async sendMessage(message) {
|
|
12
12
|
const encodedMessage = message.encode();
|
|
13
|
-
const requestBuffer = Buffer.from(encodedMessage);
|
|
13
|
+
const requestBuffer = Buffer.from(encodedMessage, 'latin1');
|
|
14
14
|
if (this.debug) {
|
|
15
15
|
console.log('Request Message:\n');
|
|
16
16
|
if (this.debugRaw) {
|
package/dist/index.js
CHANGED
|
@@ -8,10 +8,12 @@ export * from './bpd.js';
|
|
|
8
8
|
export * from './client.js';
|
|
9
9
|
export * from './config.js';
|
|
10
10
|
export * from './dialog.js';
|
|
11
|
+
export * from './electronicStatement.js';
|
|
11
12
|
export * from './httpClient.js';
|
|
12
13
|
export * from './message.js';
|
|
13
14
|
export * from './mt535parser.js';
|
|
14
15
|
export * from './mt940parser.js';
|
|
15
16
|
export * from './segment.js';
|
|
17
|
+
export { StatementFormat } from './segments/HKEKA.js';
|
|
16
18
|
export * from './statement.js';
|
|
17
19
|
export * from './upd.js';
|
|
@@ -30,7 +30,15 @@ export class CreditCardStatementInteraction extends CustomerOrderInteraction {
|
|
|
30
30
|
const valueFloatStr = value.replaceAll('.', '').replaceAll(',', '.');
|
|
31
31
|
return parseFloat(valueFloatStr);
|
|
32
32
|
}
|
|
33
|
-
|
|
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
|
+
}
|
|
@@ -40,17 +40,23 @@ export class PortfolioInteraction extends CustomerOrderInteraction {
|
|
|
40
40
|
return [hkwpd];
|
|
41
41
|
}
|
|
42
42
|
handleResponse(response, clientResponse) {
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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());
|