lib-fints 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -40
- package/dist/camtParser.js +412 -0
- package/dist/client.js +65 -82
- package/dist/dataGroups/SepaAccount.js +17 -0
- package/dist/dataGroups/SepaAccountParameters.js +16 -0
- package/dist/dialog.js +126 -108
- package/dist/httpClient.js +17 -3
- package/dist/index.js +1 -0
- package/dist/interactions/balanceInteraction.js +1 -1
- package/dist/interactions/customerInteraction.js +6 -3
- package/dist/interactions/endDialogInteraction.js +17 -0
- package/dist/interactions/initDialogInteraction.js +18 -8
- package/dist/interactions/sepaAccountInteraction.js +48 -0
- package/dist/interactions/statementInteractionCAMT.js +54 -0
- package/dist/interactions/{statementInteraction.js → statementInteractionMT940.js} +11 -8
- package/dist/interactions/tanMediaInteraction.js +4 -0
- package/dist/segments/HICAZ.js +22 -0
- package/dist/segments/HICAZS.js +19 -0
- package/dist/segments/HISPA.js +14 -0
- package/dist/segments/HISPAS.js +14 -0
- package/dist/segments/HKCAZ.js +28 -0
- package/dist/segments/HKSPA.js +21 -0
- package/dist/segments/registry.js +16 -2
- package/dist/statement.js +1 -0
- package/dist/tests/HICAZS.test.js +18 -0
- package/dist/tests/HKCAZ.test.js +47 -0
- package/dist/tests/camtParser.test.js +673 -0
- package/dist/tests/client.test.js +642 -23
- package/dist/tests/dialog.test.js +234 -0
- package/dist/types/bankAccount.d.ts +2 -3
- package/dist/types/bankAccount.d.ts.map +1 -1
- package/dist/types/camtParser.d.ts +31 -0
- package/dist/types/camtParser.d.ts.map +1 -0
- package/dist/types/client.d.ts +10 -10
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/config.d.ts +2 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/dataGroups/SepaAccount.d.ts +11 -0
- package/dist/types/dataGroups/SepaAccount.d.ts.map +1 -0
- package/dist/types/dataGroups/SepaAccountParameters.d.ts +13 -0
- package/dist/types/dataGroups/SepaAccountParameters.d.ts.map +1 -0
- package/dist/types/dialog.d.ts +12 -8
- package/dist/types/dialog.d.ts.map +1 -1
- package/dist/types/httpClient.d.ts +2 -1
- package/dist/types/httpClient.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/interactions/customerInteraction.d.ts +7 -3
- package/dist/types/interactions/customerInteraction.d.ts.map +1 -1
- package/dist/types/interactions/endDialogInteraction.d.ts +13 -0
- package/dist/types/interactions/endDialogInteraction.d.ts.map +1 -0
- package/dist/types/interactions/initDialogInteraction.d.ts +2 -3
- package/dist/types/interactions/initDialogInteraction.d.ts.map +1 -1
- package/dist/types/interactions/sepaAccountInteraction.d.ts +17 -0
- package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -0
- package/dist/types/interactions/{statementInteraction.d.ts → statementInteractionCAMT.d.ts} +4 -7
- package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -0
- package/dist/types/interactions/statementInteractionMT940.d.ts +13 -0
- package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -0
- package/dist/types/interactions/tanMediaInteraction.d.ts.map +1 -1
- package/dist/types/mt940parser.d.ts +2 -38
- package/dist/types/mt940parser.d.ts.map +1 -1
- package/dist/types/segments/HICAZ.d.ts +22 -0
- package/dist/types/segments/HICAZ.d.ts.map +1 -0
- package/dist/types/segments/HICAZS.d.ts +17 -0
- package/dist/types/segments/HICAZS.d.ts.map +1 -0
- package/dist/types/segments/HISPA.d.ts +17 -0
- package/dist/types/segments/HISPA.d.ts.map +1 -0
- package/dist/types/segments/HISPAS.d.ts +14 -0
- package/dist/types/segments/HISPAS.d.ts.map +1 -0
- package/dist/types/segments/HKCAZ.d.ts +27 -0
- package/dist/types/segments/HKCAZ.d.ts.map +1 -0
- package/dist/types/segments/HKKAZ.d.ts +1 -1
- package/dist/types/segments/HKSAL.d.ts +1 -1
- package/dist/types/segments/HKSPA.d.ts +21 -0
- package/dist/types/segments/HKSPA.d.ts.map +1 -0
- package/dist/types/segments/HKTAN.d.ts +1 -1
- package/dist/types/segments/registry.d.ts.map +1 -1
- package/dist/types/statement.d.ts +39 -0
- package/dist/types/statement.d.ts.map +1 -0
- package/dist/types/tests/HICAZS.test.d.ts +2 -0
- package/dist/types/tests/HICAZS.test.d.ts.map +1 -0
- package/dist/types/tests/HKCAZ.test.d.ts +2 -0
- package/dist/types/tests/HKCAZ.test.d.ts.map +1 -0
- package/dist/types/tests/camtParser.test.d.ts +2 -0
- package/dist/types/tests/camtParser.test.d.ts.map +1 -0
- package/dist/types/tests/dialog.test.d.ts +2 -0
- package/dist/types/tests/dialog.test.d.ts.map +1 -0
- package/package.json +4 -1
- package/dist/types/interactions/statementInteraction.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -28,12 +28,14 @@ In theory the library is compatible with a browser environment, but communicatin
|
|
|
28
28
|
|
|
29
29
|
### Installing
|
|
30
30
|
|
|
31
|
-
Installation is straight forward by simply adding the npm package.
|
|
31
|
+
Installation is straight forward by simply adding the npm package.
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
npm i lib-fints
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
**Dependencies**: The library includes the `fast-xml-parser` package for robust CAMT statement parsing but has no other runtime dependencies.
|
|
38
|
+
|
|
37
39
|
### Sample Usage
|
|
38
40
|
|
|
39
41
|
The main public API of this library is the `FinTSClient` class and `FinTSConfig` class. In order to instantiate the client you need to provide a configuration instance. There are basically two ways to initialize a configuration object, one is when you communicate with a bank for the first time and the other when you already have banking information from a prevous session available (more on that later).
|
|
@@ -59,10 +61,10 @@ If the call is successfull the response will contain a `bankingInformation` obje
|
|
|
59
61
|
|
|
60
62
|
```typescript
|
|
61
63
|
export type BankingInformation = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
systemId: string;
|
|
65
|
+
bpd?: BPD;
|
|
66
|
+
upd?: UPD;
|
|
67
|
+
bankMessages: BankMessage[];
|
|
66
68
|
};
|
|
67
69
|
```
|
|
68
70
|
|
|
@@ -104,22 +106,22 @@ Most transactions may require authorization with a two step TAN process. As ment
|
|
|
104
106
|
```typescript
|
|
105
107
|
// we use the node readline interface later to ask the user for a TAN
|
|
106
108
|
const rl = readline.createInterface({
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
input: process.stdin,
|
|
110
|
+
output: process.stdout,
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
let response = await client.getAccountStatements(account.accountNumber);
|
|
112
114
|
|
|
113
115
|
if (!response.success) {
|
|
114
|
-
|
|
116
|
+
return;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
// need to check if a TAN is required to continue the transaction
|
|
118
120
|
if (response.requiresTan) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// asking the user for the TAN, using the tanChallenge property
|
|
122
|
+
const tan = await rl.question(response.tanChallenge + ': ');
|
|
123
|
+
// continue the transaction by providing the tanReference from the response and the entered TAN
|
|
124
|
+
response = await client.getAccountStatementsWithTan(response.tanReference!, tan);
|
|
123
125
|
}
|
|
124
126
|
```
|
|
125
127
|
|
|
@@ -140,13 +142,13 @@ This not only saves you from making the same synchronization requests every time
|
|
|
140
142
|
|
|
141
143
|
```typescript
|
|
142
144
|
const config = FinTSConfig.fromBankingInformation(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
productId,
|
|
146
|
+
productVersion,
|
|
147
|
+
bankingInformation,
|
|
148
|
+
userId,
|
|
149
|
+
pin,
|
|
150
|
+
tanMethodId,
|
|
151
|
+
tanMediaName // when also needed (see below)
|
|
150
152
|
);
|
|
151
153
|
const client = new FinTSClient(config);
|
|
152
154
|
```
|
|
@@ -161,12 +163,12 @@ You can get a list of all available TAN methods from the `config.availableTanMet
|
|
|
161
163
|
|
|
162
164
|
```typescript
|
|
163
165
|
export type TanMethod = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
id: number;
|
|
167
|
+
name: string;
|
|
168
|
+
version: number;
|
|
169
|
+
activeTanMediaCount: number;
|
|
170
|
+
activeTanMedia: string[];
|
|
171
|
+
tanMediaRequirement: TanMediaRequirement;
|
|
170
172
|
};
|
|
171
173
|
```
|
|
172
174
|
|
|
@@ -192,26 +194,26 @@ This will print out all sent messages and received responses to the console in a
|
|
|
192
194
|
|
|
193
195
|
The following table shows all transactions supported by the FinTSClient interface:
|
|
194
196
|
|
|
195
|
-
| Transaction | Method | Description
|
|
196
|
-
| -------------------------- | -------------------------------------------------------------------- |
|
|
197
|
-
| **Synchronization** | `synchronize()` | Synchronizes bank and account information, updating config.bankingInformation
|
|
198
|
-
| **Account Balance** | `getAccountBalance(accountNumber)` | Fetches the current balance for a specific account
|
|
199
|
-
| **Account Statements** | `getAccountStatements(accountNumber, from?, to?)` | Fetches account transactions/statements for a date range
|
|
200
|
-
| **Portfolio** | `getPortfolio(accountNumber, currency?, priceQuality?, maxEntries?)` | Fetches securities portfolio information for depot accounts
|
|
201
|
-
| **Credit Card Statements** | `getCreditCardStatements(accountNumber, from?)` | Fetches credit card statements for credit card accounts
|
|
202
|
-
| **TAN Method Selection** | `selectTanMethod(tanMethodId)` | Selects a TAN method by ID from available methods
|
|
203
|
-
| **TAN Media Selection** | `selectTanMedia(tanMediaName)` | Selects a specific TAN media device by name
|
|
197
|
+
| Transaction | Method | Description | FinTS Segment(s) | TAN Support | Account-Specific |
|
|
198
|
+
| -------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------- | ----------- | ---------------- |
|
|
199
|
+
| **Synchronization** | `synchronize()` | Synchronizes bank and account information, updating config.bankingInformation | HKIDN, HKVVB, HKSYN, HKTAB | ✓ | ❌ |
|
|
200
|
+
| **Account Balance** | `getAccountBalance(accountNumber)` | Fetches the current balance for a specific account | HKSAL | ✓ | ✓ |
|
|
201
|
+
| **Account Statements** | `getAccountStatements(accountNumber, from?, to?)` | Fetches account transactions/statements for a date range (MT940 or CAMT format) | HKKAZ, HKCAZ | ✓ | ✓ |
|
|
202
|
+
| **Portfolio** | `getPortfolio(accountNumber, currency?, priceQuality?, maxEntries?)` | Fetches securities portfolio information for depot accounts | HKWPD | ✓ | ✓ |
|
|
203
|
+
| **Credit Card Statements** | `getCreditCardStatements(accountNumber, from?)` | Fetches credit card statements for credit card accounts | DKKKU | ✓ | ✓ |
|
|
204
|
+
| **TAN Method Selection** | `selectTanMethod(tanMethodId)` | Selects a TAN method by ID from available methods | - | ❌ | ❌ |
|
|
205
|
+
| **TAN Media Selection** | `selectTanMedia(tanMediaName)` | Selects a specific TAN media device by name | - | ❌ | ❌ |
|
|
204
206
|
|
|
205
207
|
### Transaction Support Checking
|
|
206
208
|
|
|
207
209
|
For each account-specific transaction, the client provides corresponding `can*` methods to check if the bank or specific account supports the transaction:
|
|
208
210
|
|
|
209
|
-
| Support Check Method | Purpose
|
|
210
|
-
| -------------------------------------------- |
|
|
211
|
-
| `canGetAccountBalance(accountNumber?)` | Checks if account balance fetching is supported
|
|
212
|
-
| `canGetAccountStatements(accountNumber?)` | Checks if account statements fetching is supported
|
|
213
|
-
| `canGetPortfolio(accountNumber?)` | Checks if portfolio information fetching is supported
|
|
214
|
-
| `canGetCreditCardStatements(accountNumber?)` | Checks if credit card statements fetching is supported
|
|
211
|
+
| Support Check Method | Purpose |
|
|
212
|
+
| -------------------------------------------- | --------------------------------------------------------------- |
|
|
213
|
+
| `canGetAccountBalance(accountNumber?)` | Checks if account balance fetching is supported |
|
|
214
|
+
| `canGetAccountStatements(accountNumber?)` | Checks if account statements fetching is supported (MT940/CAMT) |
|
|
215
|
+
| `canGetPortfolio(accountNumber?)` | Checks if portfolio information fetching is supported |
|
|
216
|
+
| `canGetCreditCardStatements(accountNumber?)` | Checks if credit card statements fetching is supported |
|
|
215
217
|
|
|
216
218
|
### TAN Continuation Methods
|
|
217
219
|
|
|
@@ -242,6 +244,7 @@ Implementing further transactions should be straight forward and contributions a
|
|
|
242
244
|
## Built With
|
|
243
245
|
|
|
244
246
|
- [Typescript](https://www.typescriptlang.org/) - Programming Language
|
|
247
|
+
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) - XML parsing for CAMT statements
|
|
245
248
|
- [Vitest](https://vitest.dev/) - Testing Framework
|
|
246
249
|
- [pnpm](https://pnpm.io/) - Package manager
|
|
247
250
|
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { XMLParser, XMLValidator } from 'fast-xml-parser';
|
|
2
|
+
export class CamtParsingError extends Error {
|
|
3
|
+
cause;
|
|
4
|
+
constructor(message, cause) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
this.name = 'CamtParsingError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class CamtParser {
|
|
11
|
+
xmlData;
|
|
12
|
+
parser;
|
|
13
|
+
constructor(xmlData) {
|
|
14
|
+
this.xmlData = xmlData;
|
|
15
|
+
this.parser = new XMLParser({
|
|
16
|
+
ignoreAttributes: false,
|
|
17
|
+
attributeNamePrefix: '@',
|
|
18
|
+
textNodeName: '#text',
|
|
19
|
+
removeNSPrefix: true,
|
|
20
|
+
parseAttributeValue: true,
|
|
21
|
+
trimValues: true,
|
|
22
|
+
parseTagValue: false, // Don't auto-parse values to preserve strings like "00001"
|
|
23
|
+
processEntities: true,
|
|
24
|
+
allowBooleanAttributes: false,
|
|
25
|
+
numberParseOptions: {
|
|
26
|
+
hex: false,
|
|
27
|
+
leadingZeros: true,
|
|
28
|
+
eNotation: true,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
parse() {
|
|
33
|
+
try {
|
|
34
|
+
// Pre-validate XML
|
|
35
|
+
const validationResult = XMLValidator.validate(this.xmlData);
|
|
36
|
+
if (validationResult !== true) {
|
|
37
|
+
throw new CamtParsingError(`Invalid CAMT XML structure: ${validationResult.err.msg}`);
|
|
38
|
+
}
|
|
39
|
+
// Parse XML to JavaScript object
|
|
40
|
+
const document = this.parser.parse(this.xmlData);
|
|
41
|
+
// Navigate to Document/BkToCstmrStmt/Stmt array
|
|
42
|
+
const statements = [];
|
|
43
|
+
const docObj = this.getDocumentObject(document);
|
|
44
|
+
const reports = this.getReports(docObj);
|
|
45
|
+
if (!reports || reports.length === 0) {
|
|
46
|
+
return statements;
|
|
47
|
+
}
|
|
48
|
+
for (let i = 0; i < reports.length; i++) {
|
|
49
|
+
try {
|
|
50
|
+
const statement = this.parseReport(reports[i], i + 1);
|
|
51
|
+
if (statement) {
|
|
52
|
+
statements.push(statement);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new CamtParsingError(`Failed to parse CAMT report ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return statements;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof CamtParsingError) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
throw new CamtParsingError(`Failed to parse CAMT document: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
getDocumentObject(document) {
|
|
69
|
+
// Handle different possible XML root structures
|
|
70
|
+
if (document.Document) {
|
|
71
|
+
return document.Document;
|
|
72
|
+
}
|
|
73
|
+
if (document.camt) {
|
|
74
|
+
return document.camt;
|
|
75
|
+
}
|
|
76
|
+
// Look for any object with BkToCstmrAcctRpt property
|
|
77
|
+
for (const key in document) {
|
|
78
|
+
if (document[key] && document[key].BkToCstmrAcctRpt) {
|
|
79
|
+
return document[key];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw new CamtParsingError('No valid CAMT document structure found');
|
|
83
|
+
}
|
|
84
|
+
getReports(docObj) {
|
|
85
|
+
const bkToCstmrAcctRpt = docObj.BkToCstmrAcctRpt;
|
|
86
|
+
if (!bkToCstmrAcctRpt) {
|
|
87
|
+
throw new CamtParsingError('No BkToCstmrAcctRpt element found in CAMT document');
|
|
88
|
+
}
|
|
89
|
+
const rpt = bkToCstmrAcctRpt.Rpt;
|
|
90
|
+
if (!rpt) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
// Handle both single report and array of reports
|
|
94
|
+
return Array.isArray(rpt) ? rpt : [rpt];
|
|
95
|
+
}
|
|
96
|
+
parseReport(report, reportNumber) {
|
|
97
|
+
try {
|
|
98
|
+
// Extract account information
|
|
99
|
+
const account = this.getValueFromPath(report, 'Acct.Id.IBAN');
|
|
100
|
+
// Extract statement number/ID
|
|
101
|
+
const number = this.getValueFromPath(report, 'Id');
|
|
102
|
+
// Extract transaction reference
|
|
103
|
+
const transactionReference = this.getValueFromPath(report, 'ElctrncSeqNb');
|
|
104
|
+
// Parse balances
|
|
105
|
+
const balances = this.parseBalances(report, reportNumber);
|
|
106
|
+
// Be more flexible with balance requirements - some banks only provide one balance
|
|
107
|
+
let openingBalance = balances.openingBalance;
|
|
108
|
+
let closingBalance = balances.closingBalance;
|
|
109
|
+
// If we don't have both opening and closing, try to use what we have
|
|
110
|
+
if (!openingBalance && !closingBalance) {
|
|
111
|
+
// If we have available balance, use it as closing balance
|
|
112
|
+
if (balances.availableBalance) {
|
|
113
|
+
closingBalance = balances.availableBalance;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
throw new CamtParsingError(`No balance information found in CAMT report ${reportNumber}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// If missing opening balance, create a zero balance for the same date as closing
|
|
120
|
+
if (!openingBalance && closingBalance) {
|
|
121
|
+
openingBalance = {
|
|
122
|
+
date: closingBalance.date,
|
|
123
|
+
currency: closingBalance.currency,
|
|
124
|
+
value: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// If missing closing balance, use opening balance as closing
|
|
128
|
+
if (!closingBalance && openingBalance) {
|
|
129
|
+
closingBalance = openingBalance;
|
|
130
|
+
}
|
|
131
|
+
// Parse transactions
|
|
132
|
+
const transactions = this.parseTransactions(report, reportNumber);
|
|
133
|
+
return {
|
|
134
|
+
account,
|
|
135
|
+
number,
|
|
136
|
+
transactionReference,
|
|
137
|
+
openingBalance: openingBalance,
|
|
138
|
+
closingBalance: closingBalance,
|
|
139
|
+
availableBalance: balances.availableBalance,
|
|
140
|
+
transactions,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof CamtParsingError) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
throw new CamtParsingError(`Failed to parse report ${reportNumber} content: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
getValueFromPath(obj, path) {
|
|
151
|
+
const pathParts = path.split('.');
|
|
152
|
+
let current = obj;
|
|
153
|
+
for (const part of pathParts) {
|
|
154
|
+
if (current && typeof current === 'object' && current[part] !== undefined) {
|
|
155
|
+
current = current[part];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (typeof current === 'string' || typeof current === 'number') {
|
|
162
|
+
return String(current);
|
|
163
|
+
}
|
|
164
|
+
if (current && typeof current === 'object' && current['#text'] !== undefined) {
|
|
165
|
+
return String(current['#text']);
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
parseBalances(report, reportNumber) {
|
|
170
|
+
try {
|
|
171
|
+
let openingBalance;
|
|
172
|
+
let closingBalance;
|
|
173
|
+
let availableBalance;
|
|
174
|
+
// Get balance array from report
|
|
175
|
+
const balances = report.Bal;
|
|
176
|
+
if (!balances) {
|
|
177
|
+
return { openingBalance, closingBalance, availableBalance };
|
|
178
|
+
}
|
|
179
|
+
const balanceArray = Array.isArray(balances) ? balances : [balances];
|
|
180
|
+
for (const balanceObj of balanceArray) {
|
|
181
|
+
const typeCode = this.getValueFromPath(balanceObj, 'Tp.CdOrPrtry.Cd');
|
|
182
|
+
// Extract amount and currency
|
|
183
|
+
const currency = balanceObj.Amt?.['@Ccy'] || 'EUR';
|
|
184
|
+
const value = parseFloat(this.getValueFromPath(balanceObj, 'Amt') || '0');
|
|
185
|
+
const creditDebitInd = this.getValueFromPath(balanceObj, 'CdtDbtInd');
|
|
186
|
+
const finalValue = creditDebitInd === 'DBIT' ? -value : value;
|
|
187
|
+
const dateStr = this.getValueFromPath(balanceObj, 'Dt.Dt') || this.getValueFromPath(balanceObj, 'Dt');
|
|
188
|
+
const date = dateStr ? this.parseDate(dateStr) : new Date();
|
|
189
|
+
const balance = {
|
|
190
|
+
date,
|
|
191
|
+
currency,
|
|
192
|
+
value: finalValue,
|
|
193
|
+
};
|
|
194
|
+
switch (typeCode) {
|
|
195
|
+
case 'PRCD': // Previous closing date
|
|
196
|
+
case 'OPBD': // Opening booked
|
|
197
|
+
case 'OPAV': // Opening available
|
|
198
|
+
openingBalance = balance;
|
|
199
|
+
break;
|
|
200
|
+
case 'CLBD': // Closing booked
|
|
201
|
+
case 'CLAV': // Closing available
|
|
202
|
+
closingBalance = balance;
|
|
203
|
+
break;
|
|
204
|
+
case 'ITBD': // Interim booked
|
|
205
|
+
case 'ITAV': // Interim available
|
|
206
|
+
case 'FWAV': // Forward available
|
|
207
|
+
case 'BOOK': // Booked balance
|
|
208
|
+
// Use as available balance, or as closing if we don't have one
|
|
209
|
+
if (!availableBalance) {
|
|
210
|
+
availableBalance = balance;
|
|
211
|
+
}
|
|
212
|
+
// If we don't have a closing balance, use this as closing
|
|
213
|
+
if (!closingBalance && (typeCode === 'BOOK' || typeCode === 'ITBD')) {
|
|
214
|
+
closingBalance = balance;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
// Handle unknown balance types by using them as closing balance if we don't have one
|
|
219
|
+
if (!closingBalance) {
|
|
220
|
+
closingBalance = balance;
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { openingBalance, closingBalance, availableBalance };
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
throw new CamtParsingError(`Failed to parse balances in report ${reportNumber}: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
parseTransactions(report, reportNumber) {
|
|
232
|
+
const transactions = [];
|
|
233
|
+
const entries = report.Ntry;
|
|
234
|
+
if (!entries) {
|
|
235
|
+
return transactions;
|
|
236
|
+
}
|
|
237
|
+
const entryArray = Array.isArray(entries) ? entries : [entries];
|
|
238
|
+
for (let i = 0; i < entryArray.length; i++) {
|
|
239
|
+
try {
|
|
240
|
+
const transaction = this.parseTransaction(entryArray[i], reportNumber, i + 1);
|
|
241
|
+
if (transaction) {
|
|
242
|
+
transactions.push(transaction);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
throw new CamtParsingError(`Failed to parse transaction ${i + 1} in report ${reportNumber}: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return transactions;
|
|
250
|
+
}
|
|
251
|
+
parseTransaction(entry, reportNumber, transactionNumber) {
|
|
252
|
+
try {
|
|
253
|
+
// Extract amount and credit/debit indicator
|
|
254
|
+
const amountValue = parseFloat(this.getValueFromPath(entry, 'Amt') || '0');
|
|
255
|
+
const creditDebitInd = this.getValueFromPath(entry, 'CdtDbtInd');
|
|
256
|
+
const isDebit = creditDebitInd === 'DBIT';
|
|
257
|
+
const amount = isDebit ? -amountValue : amountValue;
|
|
258
|
+
// Extract dates
|
|
259
|
+
const bookingDate = this.getValueFromPath(entry, 'BookgDt.Dt') || this.getValueFromPath(entry, 'BookgDt');
|
|
260
|
+
const valueDate = this.getValueFromPath(entry, 'ValDt.Dt') || this.getValueFromPath(entry, 'ValDt');
|
|
261
|
+
const entryDate = bookingDate ? this.parseDate(bookingDate) : new Date();
|
|
262
|
+
const parsedValueDate = valueDate ? this.parseDate(valueDate) : entryDate;
|
|
263
|
+
// Extract references
|
|
264
|
+
const accountServicerRef = this.getValueFromPath(entry, 'AcctSvcrRef') || '';
|
|
265
|
+
const endToEndId = this.getValueFromPath(entry, 'NtryDtls.TxDtls.Refs.EndToEndId') || '';
|
|
266
|
+
const mandateId = this.getValueFromPath(entry, 'NtryDtls.TxDtls.Refs.MndtId') || '';
|
|
267
|
+
// Extract transaction details
|
|
268
|
+
const additionalEntryInfo = this.getValueFromPath(entry, 'AddtlNtryInf') || '';
|
|
269
|
+
const remittanceInfo = this.getValueFromPath(entry, 'NtryDtls.TxDtls.RmtInf.Ustrd') || '';
|
|
270
|
+
// Extract remote party information based on transaction type
|
|
271
|
+
let remoteName = '';
|
|
272
|
+
let remoteIBAN = '';
|
|
273
|
+
let remoteBankId = '';
|
|
274
|
+
const txDtls = entry.NtryDtls?.TxDtls;
|
|
275
|
+
if (txDtls) {
|
|
276
|
+
if (isDebit) {
|
|
277
|
+
// For debit transactions, we want the creditor (receiving party)
|
|
278
|
+
remoteName = this.extractPartyName(txDtls, 'RltdPties.Cdtr');
|
|
279
|
+
remoteIBAN = this.getValueFromPath(txDtls, 'RltdPties.CdtrAcct.Id.IBAN') || '';
|
|
280
|
+
remoteBankId = this.extractBankId(txDtls, 'RltdAgts.CdtrAgt.FinInstnId');
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// For credit transactions, we want the debtor (sending party)
|
|
284
|
+
remoteName = this.extractPartyName(txDtls, 'RltdPties.Dbtr');
|
|
285
|
+
remoteIBAN = this.getValueFromPath(txDtls, 'RltdPties.DbtrAcct.Id.IBAN') || '';
|
|
286
|
+
remoteBankId = this.extractBankId(txDtls, 'RltdAgts.DbtrAgt.FinInstnId');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Extract bank transaction code structure (BkTxCd) - can be at entry level or TxDtls level
|
|
290
|
+
let bkTxCd = this.parseBankTransactionCode(entry);
|
|
291
|
+
if (!bkTxCd.domainCode && !bkTxCd.familyCode && !bkTxCd.subFamilyCode && txDtls) {
|
|
292
|
+
bkTxCd = this.parseBankTransactionCode(txDtls);
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
valueDate: parsedValueDate,
|
|
296
|
+
entryDate,
|
|
297
|
+
fundsCode: bkTxCd.domainCode || creditDebitInd || '',
|
|
298
|
+
amount,
|
|
299
|
+
transactionType: bkTxCd.familyCode || '',
|
|
300
|
+
customerReference: endToEndId,
|
|
301
|
+
bankReference: accountServicerRef,
|
|
302
|
+
transactionCode: bkTxCd.subFamilyCode || '',
|
|
303
|
+
purpose: remittanceInfo,
|
|
304
|
+
remoteName,
|
|
305
|
+
remoteAccountNumber: remoteIBAN,
|
|
306
|
+
remoteBankId,
|
|
307
|
+
e2eReference: endToEndId,
|
|
308
|
+
mandateReference: mandateId,
|
|
309
|
+
additionalInformation: additionalEntryInfo,
|
|
310
|
+
bookingText: additionalEntryInfo,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
throw new CamtParsingError(`Failed to parse transaction details: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Extract party name from various possible CAMT structures
|
|
319
|
+
* Handles both direct name (<Dbtr><Nm>) and party structure (<Dbtr><Pty><Nm>)
|
|
320
|
+
*/
|
|
321
|
+
extractPartyName(txDtls, partyPath) {
|
|
322
|
+
// Strategy 1: Direct name structure (e.g., RltdPties.Dbtr.Nm)
|
|
323
|
+
let name = this.getValueFromPath(txDtls, `${partyPath}.Nm`);
|
|
324
|
+
if (name) {
|
|
325
|
+
return name;
|
|
326
|
+
}
|
|
327
|
+
// Strategy 2: Party structure (e.g., RltdPties.Dbtr.Pty.Nm)
|
|
328
|
+
name = this.getValueFromPath(txDtls, `${partyPath}.Pty.Nm`);
|
|
329
|
+
if (name) {
|
|
330
|
+
return name;
|
|
331
|
+
}
|
|
332
|
+
// Strategy 3: Organization ID structure (e.g., RltdPties.Dbtr.Id.OrgId.Nm)
|
|
333
|
+
name = this.getValueFromPath(txDtls, `${partyPath}.Id.OrgId.Nm`);
|
|
334
|
+
if (name) {
|
|
335
|
+
return name;
|
|
336
|
+
}
|
|
337
|
+
// Strategy 4: Private ID structure (e.g., RltdPties.Dbtr.Id.PrvtId.Nm)
|
|
338
|
+
name = this.getValueFromPath(txDtls, `${partyPath}.Id.PrvtId.Nm`);
|
|
339
|
+
if (name) {
|
|
340
|
+
return name;
|
|
341
|
+
}
|
|
342
|
+
// Strategy 5: Try postal address line as fallback
|
|
343
|
+
name = this.getValueFromPath(txDtls, `${partyPath}.PstlAdr.AdrLine`);
|
|
344
|
+
if (name) {
|
|
345
|
+
return name;
|
|
346
|
+
}
|
|
347
|
+
// Strategy 6: Try organization identification other
|
|
348
|
+
name = this.getValueFromPath(txDtls, `${partyPath}.Id.OrgId.Othr.Id`);
|
|
349
|
+
if (name) {
|
|
350
|
+
return name;
|
|
351
|
+
}
|
|
352
|
+
return '';
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Extract bank identification code from various possible CAMT structures
|
|
356
|
+
* Handles both BIC and BICFI elements
|
|
357
|
+
*/
|
|
358
|
+
extractBankId(txDtls, bankPath) {
|
|
359
|
+
// Strategy 1: Standard BIC element
|
|
360
|
+
let bankId = this.getValueFromPath(txDtls, `${bankPath}.BIC`);
|
|
361
|
+
if (bankId) {
|
|
362
|
+
return bankId;
|
|
363
|
+
}
|
|
364
|
+
// Strategy 2: BICFI element (used by some banks)
|
|
365
|
+
bankId = this.getValueFromPath(txDtls, `${bankPath}.BICFI`);
|
|
366
|
+
if (bankId) {
|
|
367
|
+
return bankId;
|
|
368
|
+
}
|
|
369
|
+
// Strategy 3: Try ClrSysMmbId (clearing system member identification)
|
|
370
|
+
bankId = this.getValueFromPath(txDtls, `${bankPath}.ClrSysMmbId.MmbId`);
|
|
371
|
+
if (bankId) {
|
|
372
|
+
return bankId;
|
|
373
|
+
}
|
|
374
|
+
// Strategy 4: Try other identification
|
|
375
|
+
bankId = this.getValueFromPath(txDtls, `${bankPath}.Othr.Id`);
|
|
376
|
+
if (bankId) {
|
|
377
|
+
return bankId;
|
|
378
|
+
}
|
|
379
|
+
return '';
|
|
380
|
+
}
|
|
381
|
+
parseDate(dateStr) {
|
|
382
|
+
// Parse ISO date format (YYYY-MM-DD)
|
|
383
|
+
if (dateStr.length === 10 && dateStr.includes('-')) {
|
|
384
|
+
return new Date(dateStr + 'T12:00:00'); // Set time to noon to avoid timezone issues
|
|
385
|
+
}
|
|
386
|
+
// Parse CAMT date format (YYYYMMDD)
|
|
387
|
+
if (dateStr.length === 8) {
|
|
388
|
+
const year = parseInt(dateStr.substring(0, 4), 10);
|
|
389
|
+
const month = parseInt(dateStr.substring(4, 6), 10) - 1; // Month is 0-based
|
|
390
|
+
const day = parseInt(dateStr.substring(6, 8), 10);
|
|
391
|
+
return new Date(year, month, day, 12);
|
|
392
|
+
}
|
|
393
|
+
return new Date(dateStr);
|
|
394
|
+
}
|
|
395
|
+
parseBankTransactionCode(entry) {
|
|
396
|
+
const bkTxCd = entry.BkTxCd;
|
|
397
|
+
if (!bkTxCd) {
|
|
398
|
+
return {};
|
|
399
|
+
}
|
|
400
|
+
// Extract Domain Code (first level - e.g., "PMNT")
|
|
401
|
+
const domainCode = this.getValueFromPath(bkTxCd, 'Domn.Cd');
|
|
402
|
+
// Extract Family Code (second level - e.g., "CCRD")
|
|
403
|
+
const familyCode = this.getValueFromPath(bkTxCd, 'Domn.Fmly.Cd');
|
|
404
|
+
// Extract SubFamily Code (third level - e.g., "POSD")
|
|
405
|
+
const subFamilyCode = this.getValueFromPath(bkTxCd, 'Domn.Fmly.SubFmlyCd');
|
|
406
|
+
return {
|
|
407
|
+
domainCode,
|
|
408
|
+
familyCode,
|
|
409
|
+
subFamilyCode,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|