lib-fints 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +43 -40
  2. package/dist/camtParser.js +412 -0
  3. package/dist/client.js +58 -80
  4. package/dist/dataGroups/SepaAccount.js +17 -0
  5. package/dist/dataGroups/SepaAccountParameters.js +16 -0
  6. package/dist/dialog.js +126 -108
  7. package/dist/httpClient.js +17 -3
  8. package/dist/index.js +1 -0
  9. package/dist/interactions/balanceInteraction.js +1 -1
  10. package/dist/interactions/customerInteraction.js +6 -3
  11. package/dist/interactions/endDialogInteraction.js +17 -0
  12. package/dist/interactions/initDialogInteraction.js +18 -8
  13. package/dist/interactions/sepaAccountInteraction.js +48 -0
  14. package/dist/interactions/statementInteractionCAMT.js +54 -0
  15. package/dist/interactions/{statementInteraction.js → statementInteractionMT940.js} +11 -8
  16. package/dist/interactions/tanMediaInteraction.js +4 -0
  17. package/dist/segments/HICAZ.js +22 -0
  18. package/dist/segments/HICAZS.js +19 -0
  19. package/dist/segments/HISPA.js +14 -0
  20. package/dist/segments/HISPAS.js +14 -0
  21. package/dist/segments/HKCAZ.js +28 -0
  22. package/dist/segments/HKSPA.js +21 -0
  23. package/dist/segments/registry.js +16 -2
  24. package/dist/statement.js +1 -0
  25. package/dist/tests/HICAZS.test.js +18 -0
  26. package/dist/tests/HKCAZ.test.js +47 -0
  27. package/dist/tests/camtParser.test.js +673 -0
  28. package/dist/tests/client.test.js +21 -16
  29. package/dist/tests/dialog.test.js +234 -0
  30. package/dist/types/bankAccount.d.ts +2 -3
  31. package/dist/types/bankAccount.d.ts.map +1 -1
  32. package/dist/types/camtParser.d.ts +31 -0
  33. package/dist/types/camtParser.d.ts.map +1 -0
  34. package/dist/types/client.d.ts +4 -4
  35. package/dist/types/client.d.ts.map +1 -1
  36. package/dist/types/config.d.ts +2 -1
  37. package/dist/types/config.d.ts.map +1 -1
  38. package/dist/types/dataGroups/SepaAccount.d.ts +11 -0
  39. package/dist/types/dataGroups/SepaAccount.d.ts.map +1 -0
  40. package/dist/types/dataGroups/SepaAccountParameters.d.ts +13 -0
  41. package/dist/types/dataGroups/SepaAccountParameters.d.ts.map +1 -0
  42. package/dist/types/dialog.d.ts +12 -8
  43. package/dist/types/dialog.d.ts.map +1 -1
  44. package/dist/types/httpClient.d.ts +2 -1
  45. package/dist/types/httpClient.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +2 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/interactions/customerInteraction.d.ts +7 -3
  49. package/dist/types/interactions/customerInteraction.d.ts.map +1 -1
  50. package/dist/types/interactions/endDialogInteraction.d.ts +13 -0
  51. package/dist/types/interactions/endDialogInteraction.d.ts.map +1 -0
  52. package/dist/types/interactions/initDialogInteraction.d.ts +2 -3
  53. package/dist/types/interactions/initDialogInteraction.d.ts.map +1 -1
  54. package/dist/types/interactions/sepaAccountInteraction.d.ts +17 -0
  55. package/dist/types/interactions/sepaAccountInteraction.d.ts.map +1 -0
  56. package/dist/types/interactions/{statementInteraction.d.ts → statementInteractionCAMT.d.ts} +4 -7
  57. package/dist/types/interactions/statementInteractionCAMT.d.ts.map +1 -0
  58. package/dist/types/interactions/statementInteractionMT940.d.ts +13 -0
  59. package/dist/types/interactions/statementInteractionMT940.d.ts.map +1 -0
  60. package/dist/types/interactions/tanMediaInteraction.d.ts.map +1 -1
  61. package/dist/types/mt940parser.d.ts +2 -38
  62. package/dist/types/mt940parser.d.ts.map +1 -1
  63. package/dist/types/segments/HICAZ.d.ts +22 -0
  64. package/dist/types/segments/HICAZ.d.ts.map +1 -0
  65. package/dist/types/segments/HICAZS.d.ts +17 -0
  66. package/dist/types/segments/HICAZS.d.ts.map +1 -0
  67. package/dist/types/segments/HISPA.d.ts +17 -0
  68. package/dist/types/segments/HISPA.d.ts.map +1 -0
  69. package/dist/types/segments/HISPAS.d.ts +14 -0
  70. package/dist/types/segments/HISPAS.d.ts.map +1 -0
  71. package/dist/types/segments/HKCAZ.d.ts +27 -0
  72. package/dist/types/segments/HKCAZ.d.ts.map +1 -0
  73. package/dist/types/segments/HKKAZ.d.ts +1 -1
  74. package/dist/types/segments/HKSAL.d.ts +1 -1
  75. package/dist/types/segments/HKSPA.d.ts +21 -0
  76. package/dist/types/segments/HKSPA.d.ts.map +1 -0
  77. package/dist/types/segments/HKTAN.d.ts +1 -1
  78. package/dist/types/segments/registry.d.ts.map +1 -1
  79. package/dist/types/statement.d.ts +39 -0
  80. package/dist/types/statement.d.ts.map +1 -0
  81. package/dist/types/tests/HICAZS.test.d.ts +2 -0
  82. package/dist/types/tests/HICAZS.test.d.ts.map +1 -0
  83. package/dist/types/tests/HKCAZ.test.d.ts +2 -0
  84. package/dist/types/tests/HKCAZ.test.d.ts.map +1 -0
  85. package/dist/types/tests/camtParser.test.d.ts +2 -0
  86. package/dist/types/tests/camtParser.test.d.ts.map +1 -0
  87. package/dist/types/tests/dialog.test.d.ts +2 -0
  88. package/dist/types/tests/dialog.test.d.ts.map +1 -0
  89. package/package.json +4 -1
  90. 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. The package has no further dependencies on other packages.
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
- systemId: string;
63
- bpd?: BPD;
64
- upd?: UPD;
65
- bankMessages: BankMessage[];
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
- input: process.stdin,
108
- output: process.stdout,
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
- return;
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
- // asking the user for the TAN, using the tanChallenge property
120
- const tan = await rl.question(response.tanChallenge + ': ');
121
- // continue the transaction by providing the tanReference from the response and the entered TAN
122
- response = await client.getAccountStatementsWithTan(response.tanReference!, tan);
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
- productId,
144
- productVersion,
145
- bankingInformation,
146
- userId,
147
- pin,
148
- tanMethodId,
149
- tanMediaName // when also needed (see below)
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
- id: number;
165
- name: string;
166
- version: number;
167
- activeTanMediaCount: number;
168
- activeTanMedia: string[];
169
- tanMediaRequirement: TanMediaRequirement;
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 | FinTS Segment(s) | TAN Support | Account-Specific |
196
- | -------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------- | ----------- | ---------------- |
197
- | **Synchronization** | `synchronize()` | Synchronizes bank and account information, updating config.bankingInformation | HKIDN, HKVVB, HKSYN, HKTAB | ✓ | ❌ |
198
- | **Account Balance** | `getAccountBalance(accountNumber)` | Fetches the current balance for a specific account | HKSAL | ✓ | ✓ |
199
- | **Account Statements** | `getAccountStatements(accountNumber, from?, to?)` | Fetches account transactions/statements for a date range | HKKAZ | ✓ | ✓ |
200
- | **Portfolio** | `getPortfolio(accountNumber, currency?, priceQuality?, maxEntries?)` | Fetches securities portfolio information for depot accounts | HKWPD | ✓ | ✓ |
201
- | **Credit Card Statements** | `getCreditCardStatements(accountNumber, from?)` | Fetches credit card statements for credit card accounts | DKKKU | ✓ | ✓ |
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
+ }