From 3f2c65af6b6d844232765c9ef0fd00e83aab2b8b Mon Sep 17 00:00:00 2001 From: Anton Schegg Date: Thu, 22 Jan 2026 13:25:23 +0100 Subject: [PATCH 1/7] Fix HKCAZ encoding and camtParser --- src/camtParser.ts | 25 +++- src/dataGroups/CamtAccount.ts | 23 ++++ src/segments/HKCAZ.ts | 9 +- src/tests/HKCAZ.test.ts | 10 +- src/tests/camtParser.test.ts | 237 +++++++++++++++++++++++++++------- 5 files changed, 239 insertions(+), 65 deletions(-) create mode 100644 src/dataGroups/CamtAccount.ts diff --git a/src/camtParser.ts b/src/camtParser.ts index 4e286dd..57be21b 100644 --- a/src/camtParser.ts +++ b/src/camtParser.ts @@ -353,7 +353,7 @@ export class CamtParser { return String(current); } if (Array.isArray(current)) { - return String(current.join('')); + return String(current.join('\n')); } if (current && typeof current === 'object' && current !== null && '#text' in current) { return String((current as { '#text': unknown })['#text']); @@ -488,9 +488,9 @@ export class CamtParser { // Extract dates const bookingDate = - this.getValueFromPath(entry, 'BookgDt.Dt') || this.getValueFromPath(entry, 'BookgDt'); + this.getValueFromPath(entry, 'BookgDt.DtTm') || this.getValueFromPath(entry, 'BookgDt.Dt') || this.getValueFromPath(entry, 'BookgDt'); const valueDate = - this.getValueFromPath(entry, 'ValDt.Dt') || this.getValueFromPath(entry, 'ValDt'); + this.getValueFromPath(entry, 'ValDt.DtTm') || this.getValueFromPath(entry, 'ValDt.Dt') || this.getValueFromPath(entry, 'ValDt'); const entryDate = bookingDate ? this.parseDate(bookingDate) : new Date(); const parsedValueDate = valueDate ? this.parseDate(valueDate) : entryDate; @@ -633,7 +633,24 @@ export class CamtParser { } private parseDate(dateStr: string): Date { - // Parse ISO date format (YYYY-MM-DD) + let processedDateStr = dateStr; + // Handle date-only with timezone, e.g., "2026-01-22+01:00" + // The Date constructor may not parse this correctly, so we add a time part. + if (/^\d{4}-\d{2}-\d{2}[+-]\d{2}:\d{2}$/.test(dateStr)) { + processedDateStr = `${dateStr.substring(0, 10)}T00:00:00${dateStr.substring(10)}`; + } + + // Attempt to parse as a full ISO 8601 string first, which `new Date()` handles well. + // This will correctly handle formats like "2023-10-26T10:00:00+02:00". + const isoDate = new Date(processedDateStr); + if (!isNaN(isoDate.getTime())) { + // Check if the date string contains time or timezone information to avoid misinterpreting YYYY-MM-DD + if (processedDateStr.includes('T') || /[-+]\d{2}:\d{2}$/.test(processedDateStr)) { + return isoDate; + } + } + + // Fallback for date-only ISO format (YYYY-MM-DD) if (dateStr.length === 10 && dateStr.includes('-')) { return new Date(`${dateStr}T12:00:00`); // Set time to noon to avoid timezone issues } diff --git a/src/dataGroups/CamtAccount.ts b/src/dataGroups/CamtAccount.ts new file mode 100644 index 0000000..a295297 --- /dev/null +++ b/src/dataGroups/CamtAccount.ts @@ -0,0 +1,23 @@ +import { AlphaNumeric } from '../dataElements/AlphaNumeric.js'; +import { DataGroup } from './DataGroup.js'; + +export type CamtAccount = { + iban?: string; + bic?: string; +}; + +export class CamtAccountGroup extends DataGroup { + constructor(name: string, minCount = 0, maxCount = 1, minVersion?: number, maxVersion?: number) { + super( + name, + [ + new AlphaNumeric('iban', 0, 1, 34), + new AlphaNumeric('bic', 0, 1, 11), + ], + minCount, + maxCount, + minVersion, + maxVersion, + ); + } +} diff --git a/src/segments/HKCAZ.ts b/src/segments/HKCAZ.ts index cc15151..916bbf7 100644 --- a/src/segments/HKCAZ.ts +++ b/src/segments/HKCAZ.ts @@ -4,15 +4,12 @@ import { Numeric } from '../dataElements/Numeric.js'; import { Text } from '../dataElements/Text.js'; import { YesNo } from '../dataElements/YesNo.js'; import { DataGroup } from '../dataGroups/DataGroup.js'; -import { - type InternationalAccount, - InternationalAccountGroup, -} from '../dataGroups/InternationalAccount.js'; import type { SegmentWithContinuationMark } from '../segment.js'; import { SegmentDefinition } from '../segmentDefinition.js'; +import {CamtAccount, CamtAccountGroup} from "../dataGroups/CamtAccount.js"; export type HKCAZSegment = SegmentWithContinuationMark & { - account: InternationalAccount; + account: CamtAccount; acceptedCamtFormats: string[]; allAccounts: boolean; from?: Date; @@ -31,7 +28,7 @@ export class HKCAZ extends SegmentDefinition { } version = HKCAZ.Version; elements = [ - new InternationalAccountGroup('account', 1, 1), + new CamtAccountGroup('account', 1, 1), new DataGroup('acceptedCamtFormats', [new Text('camtFormat', 1, 99)], 1, 1), // Support multiple camt-formats new YesNo('allAccounts', 1, 1), new Dat('from', 0, 1), diff --git a/src/tests/HKCAZ.test.ts b/src/tests/HKCAZ.test.ts index d0c14d7..f63148a 100644 --- a/src/tests/HKCAZ.test.ts +++ b/src/tests/HKCAZ.test.ts @@ -12,8 +12,6 @@ describe('HKCAZ v1', () => { account: { iban: 'DE991234567123456', bic: 'BANK12', - accountNumber: '123456', - bank: { country: 280, bankId: '12030000' }, }, acceptedCamtFormats: ['urn:iso:std:iso:20022:tech:xsd:camt.052.001.08'], allAccounts: false, @@ -22,7 +20,7 @@ describe('HKCAZ v1', () => { }; expect(encode(segment)).toBe( - "HKCAZ:1:1+DE991234567123456:BANK12:123456::280:12030000+urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08+N+20230101+20231231'", + "HKCAZ:1:1+DE991234567123456:BANK12+urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08+N+20230101+20231231'", ); }); @@ -32,21 +30,19 @@ describe('HKCAZ v1', () => { account: { iban: 'DE991234567123456', bic: 'BANK12', - accountNumber: '123456', - bank: { country: 280, bankId: '12030000' }, }, acceptedCamtFormats: ['urn:iso:std:iso:20022:tech:xsd:camt.052.001.08'], allAccounts: true, }; expect(encode(segment)).toBe( - "HKCAZ:2:1+DE991234567123456:BANK12:123456::280:12030000+urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08+J'", + "HKCAZ:2:1+DE991234567123456:BANK12+urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08+J'", ); }); it('decode and encode roundtrip matches', () => { const text = - "HKCAZ:0:1+DE991234567123456:BANK12:123456::280:12030000+urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08+N+20230101+20231231'"; + "HKCAZ:0:1+DE991234567123456:BANK12+urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08+N+20230101+20231231'"; const segment = decode(text); expect(encode(segment)).toBe(text); }); diff --git a/src/tests/camtParser.test.ts b/src/tests/camtParser.test.ts index f745f63..2b06c87 100644 --- a/src/tests/camtParser.test.ts +++ b/src/tests/camtParser.test.ts @@ -729,8 +729,8 @@ describe('CamtParser', () => { expect(transaction.amount).toBe(200.0); }); - it('should handle multiple entries in RmtInf (Ustrd)', () => { - const camtXml = ` + it('should handle multiple entries in RmtInf (Ustrd)', () => { + const camtXml = ` @@ -878,50 +878,191 @@ describe('CamtParser', () => { `; - const parser = new CamtParser(camtXml); - const statements = parser.parse(); - - expect(statements).toHaveLength(1); - const statement = statements[0]; - expect(statement.transactions).toHaveLength(1); - - const transaction = statement.transactions[0]; - - // Check all Transaction fields filled by the parser - expect(transaction.amount).toBe(-179.46); - expect(transaction.customerReference).toBe('VG 2025 QUARTAL IV'); - expect(transaction.bankReference).toBe('TXN003'); - expect(transaction.purpose).toBe( - '28,65EUR EREF: VG 2025 QUARTAL IV IBAN: DE12345678901234567891 BIC: BANKABC1XXX', - ); - expect(transaction.remoteName).toBe('ABC Bank'); - expect(transaction.remoteAccountNumber).toBe('DE12345678901234567891'); - expect(transaction.remoteBankId).toBe('BANKABC1XXX'); - expect(transaction.e2eReference).toBe('VG 2025 QUARTAL IV'); - - // Check date fields - expect(transaction.valueDate).toBeInstanceOf(Date); - expect(transaction.valueDate.getFullYear()).toBe(2026); - expect(transaction.valueDate.getMonth()).toBe(0); // November (0-based) - expect(transaction.valueDate.getDate()).toBe(5); - expect(transaction.entryDate).toBeInstanceOf(Date); - expect(transaction.entryDate.getFullYear()).toBe(2026); - expect(transaction.entryDate.getMonth()).toBe(0); // November (0-based) - expect(transaction.entryDate.getDate()).toBe(5); - - // Check transaction type and code fields - expect(transaction.fundsCode).toBe('PMNT'); - expect(transaction.transactionType).toBe('ICDT'); - expect(transaction.transactionCode).toBe('ESCT'); - - // Check additional information fields - expect(transaction.additionalInformation).toBe('ENTGELT gem. Vereinbarung'); - expect(transaction.bookingText).toBe('ENTGELT gem. Vereinbarung'); // Should match additionalInformation - - // Verify optional fields not set in this test - expect(transaction.primeNotesNr).toBeUndefined(); - expect(transaction.remoteIdentifier).toBeUndefined(); - expect(transaction.client).toBeUndefined(); - expect(transaction.textKeyExtension).toBeUndefined(); - }); + const parser = new CamtParser(camtXml); + const statements = parser.parse(); + + expect(statements).toHaveLength(1); + const statement = statements[0]; + expect(statement.transactions).toHaveLength(1); + + const transaction = statement.transactions[0]; + + // Check all Transaction fields filled by the parser + expect(transaction.amount).toBe(-179.46); + expect(transaction.customerReference).toBe('VG 2025 QUARTAL IV'); + expect(transaction.bankReference).toBe('TXN003'); + expect(transaction.purpose).toBe( + '28,65EUR EREF: VG 2025 QUARTAL IV IBAN\n: DE12345678901234567891 BIC: BANKABC1XXX', + ); + expect(transaction.remoteName).toBe('ABC Bank'); + expect(transaction.remoteAccountNumber).toBe('DE12345678901234567891'); + expect(transaction.remoteBankId).toBe('BANKABC1XXX'); + expect(transaction.e2eReference).toBe('VG 2025 QUARTAL IV'); + + // Check date fields + expect(transaction.valueDate).toBeInstanceOf(Date); + expect(transaction.valueDate.getFullYear()).toBe(2026); + expect(transaction.valueDate.getMonth()).toBe(0); // November (0-based) + expect(transaction.valueDate.getDate()).toBe(5); + expect(transaction.entryDate).toBeInstanceOf(Date); + expect(transaction.entryDate.getFullYear()).toBe(2026); + expect(transaction.entryDate.getMonth()).toBe(0); // November (0-based) + expect(transaction.entryDate.getDate()).toBe(5); + + // Check transaction type and code fields + expect(transaction.fundsCode).toBe('PMNT'); + expect(transaction.transactionType).toBe('ICDT'); + expect(transaction.transactionCode).toBe('ESCT'); + + // Check additional information fields + expect(transaction.additionalInformation).toBe('ENTGELT gem. Vereinbarung'); + expect(transaction.bookingText).toBe('ENTGELT gem. Vereinbarung'); // Should match additionalInformation + + // Verify optional fields not set in this test + expect(transaction.primeNotesNr).toBeUndefined(); + expect(transaction.remoteIdentifier).toBeUndefined(); + expect(transaction.client).toBeUndefined(); + expect(transaction.textKeyExtension).toBeUndefined(); + }); + + it('should handle full iso date time in value date', () => { + // this is an example from comdirect bank in 2026-01 + const camtXml = ` + + + + BD5F4D36X95740C4B89D967367217C16 + 2026-01-22T10:35:25.369+01:00 + + 0 + true + + + + 563916B991DD4EB18894EF4ABB730A5C + + 2025-12-10T00:00:00.000+01:00 + 2026-01-22T00:00:00.000+01:00 + + + + DE06940594210000027227 + + + + + + OPBD + + + 94.010000000021 + CRDT +
+ 2025-12-10T00:00:00.000+01:00 +
+
+ + + + CLBD + + + 101.960000000017 + CRDT +
+ 2026-01-22T00:00:00.000+01:00 +
+
+ + 5J3C21XL0470L56V/39761 + 101.5 + DBIT + BOOK + +
2025-12-10+01:00
+
+ + 2025-12-10T00:00:00.000+01:00 + + 5J2C21XL0470L56V/39761 + + + 005 + + + + + + + + AMAZON EU S.A R.L., NIEDERL ASSUNG DEUTSCHLAND + + + + + + + 028-1234567-XXXXXXX Amazon.de 2ABCD + EF9GFP28 + End-to-End-Ref.: + 2ABCDEF9GHIJKL28 + CORE / Mandatsref.: + 7829857lkklag + Gläubiger-ID: + DE24ABC00000123456 + + + +
+
+
+
+`; + + const parser = new CamtParser(camtXml); + const statements = parser.parse(); + + expect(statements).toHaveLength(1); + const statement = statements[0]; + expect(statement.transactions).toHaveLength(1); + + const transaction = statement.transactions[0]; + + // Check all Transaction fields filled by the parser + expect(transaction.amount).toBe(-101.5); + expect(transaction.customerReference).toBe(''); + expect(transaction.bankReference).toBe('5J2C21XL0470L56V/39761'); + expect(transaction.purpose).toBe( + '028-1234567-XXXXXXX Amazon.de 2ABCD\nEF9GFP28\nEnd-to-End-Ref.:\n2ABCDEF9GHIJKL28\nCORE / Mandatsref.:\n7829857lkklag\nGläubiger-ID:\nDE24ABC00000123456', + ); + expect(transaction.remoteName).toBe('AMAZON EU S.A R.L., NIEDERL ASSUNG DEUTSCHLAND'); + expect(transaction.remoteAccountNumber).toBe(''); + expect(transaction.remoteBankId).toBe(''); + expect(transaction.e2eReference).toBe(''); + + // Check date fields + expect(transaction.valueDate).toBeInstanceOf(Date); + expect(transaction.valueDate.getFullYear()).toBe(2025); + expect(transaction.valueDate.getMonth()).toBe(11); // November (0-based) + expect(transaction.valueDate.getDate()).toBe(10); + expect(transaction.entryDate).toBeInstanceOf(Date); + expect(transaction.entryDate.getFullYear()).toBe(2025); + expect(transaction.entryDate.getMonth()).toBe(11); // November (0-based) + expect(transaction.entryDate.getDate()).toBe(10); + + // Check transaction type and code fields + expect(transaction.fundsCode).toBe('DBIT'); + expect(transaction.transactionType).toBe(''); + expect(transaction.transactionCode).toBe(''); + + // Check additional information fields + expect(transaction.additionalInformation).toBe(''); + expect(transaction.bookingText).toBe(''); // Should match additionalInformation + + // Verify optional fields not set in this test + expect(transaction.primeNotesNr).toBeUndefined(); + expect(transaction.remoteIdentifier).toBeUndefined(); + expect(transaction.client).toBeUndefined(); + expect(transaction.textKeyExtension).toBeUndefined(); + }); }); From 702411e24262e433779ae1f6e57bcacce04f695d Mon Sep 17 00:00:00 2001 From: Anton Schegg Date: Thu, 22 Jan 2026 15:29:51 +0100 Subject: [PATCH 2/7] re-decode camtMessage as UTF-8 --- src/interactions/statementInteractionCAMT.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/interactions/statementInteractionCAMT.ts b/src/interactions/statementInteractionCAMT.ts index a6dcb67..d2a85e9 100644 --- a/src/interactions/statementInteractionCAMT.ts +++ b/src/interactions/statementInteractionCAMT.ts @@ -53,7 +53,13 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction { // Parse all CAMT messages (one per booking day) and combine statements const allStatements: Statement[] = []; for (const camtMessage of hicaz.bookedTransactions) { - const parser = new CamtParser(camtMessage); + + // camtMessage is initially encoded as 'latin1' (ISO-8859-1), but actually contains UTF-8 data. + // Therefore, we need to first convert it back to a buffer using 'latin1', and then decode it as 'utf8'. + const intermediateBuffer = Buffer.from(camtMessage, 'latin1'); + const utf8String = intermediateBuffer.toString('utf8'); + + const parser = new CamtParser(utf8String); const statements = parser.parse(); allStatements.push(...statements); } From 075184287035e2e3579391eb9ccab6bb036a5046 Mon Sep 17 00:00:00 2001 From: Anton Schegg Date: Thu, 22 Jan 2026 15:33:03 +0100 Subject: [PATCH 3/7] Corrected wrongly converted umlaut in test --- src/tests/camtParser.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/camtParser.test.ts b/src/tests/camtParser.test.ts index 2b06c87..22d0775 100644 --- a/src/tests/camtParser.test.ts +++ b/src/tests/camtParser.test.ts @@ -1008,7 +1008,7 @@ describe('CamtParser', () => { 2ABCDEF9GHIJKL28 CORE / Mandatsref.: 7829857lkklag - Gläubiger-ID: + Gläubiger-ID: DE24ABC00000123456 @@ -1033,7 +1033,7 @@ describe('CamtParser', () => { expect(transaction.customerReference).toBe(''); expect(transaction.bankReference).toBe('5J2C21XL0470L56V/39761'); expect(transaction.purpose).toBe( - '028-1234567-XXXXXXX Amazon.de 2ABCD\nEF9GFP28\nEnd-to-End-Ref.:\n2ABCDEF9GHIJKL28\nCORE / Mandatsref.:\n7829857lkklag\nGläubiger-ID:\nDE24ABC00000123456', + '028-1234567-XXXXXXX Amazon.de 2ABCD\nEF9GFP28\nEnd-to-End-Ref.:\n2ABCDEF9GHIJKL28\nCORE / Mandatsref.:\n7829857lkklag\nGläubiger-ID:\nDE24ABC00000123456', ); expect(transaction.remoteName).toBe('AMAZON EU S.A R.L., NIEDERL ASSUNG DEUTSCHLAND'); expect(transaction.remoteAccountNumber).toBe(''); From aa92e7837847ffd58a45f33319d0368d919e15a7 Mon Sep 17 00:00:00 2001 From: Anton Schegg Date: Thu, 22 Jan 2026 15:58:02 +0100 Subject: [PATCH 4/7] Fixed linter errors with biome --- src/camtParser.ts | 10 +- src/dataGroups/CamtAccount.ts | 27 ++- src/interactions/statementInteractionCAMT.ts | 11 +- src/segments/HKCAZ.ts | 2 +- src/tests/camtParser.test.ts | 196 +++++++++---------- 5 files changed, 123 insertions(+), 123 deletions(-) diff --git a/src/camtParser.ts b/src/camtParser.ts index 57be21b..9d7207e 100644 --- a/src/camtParser.ts +++ b/src/camtParser.ts @@ -488,9 +488,13 @@ export class CamtParser { // Extract dates const bookingDate = - this.getValueFromPath(entry, 'BookgDt.DtTm') || this.getValueFromPath(entry, 'BookgDt.Dt') || this.getValueFromPath(entry, 'BookgDt'); + this.getValueFromPath(entry, 'BookgDt.DtTm') || + this.getValueFromPath(entry, 'BookgDt.Dt') || + this.getValueFromPath(entry, 'BookgDt'); const valueDate = - this.getValueFromPath(entry, 'ValDt.DtTm') || this.getValueFromPath(entry, 'ValDt.Dt') || this.getValueFromPath(entry, 'ValDt'); + this.getValueFromPath(entry, 'ValDt.DtTm') || + this.getValueFromPath(entry, 'ValDt.Dt') || + this.getValueFromPath(entry, 'ValDt'); const entryDate = bookingDate ? this.parseDate(bookingDate) : new Date(); const parsedValueDate = valueDate ? this.parseDate(valueDate) : entryDate; @@ -643,7 +647,7 @@ export class CamtParser { // Attempt to parse as a full ISO 8601 string first, which `new Date()` handles well. // This will correctly handle formats like "2023-10-26T10:00:00+02:00". const isoDate = new Date(processedDateStr); - if (!isNaN(isoDate.getTime())) { + if (!Number.isNaN(isoDate.getTime())) { // Check if the date string contains time or timezone information to avoid misinterpreting YYYY-MM-DD if (processedDateStr.includes('T') || /[-+]\d{2}:\d{2}$/.test(processedDateStr)) { return isoDate; diff --git a/src/dataGroups/CamtAccount.ts b/src/dataGroups/CamtAccount.ts index a295297..2519ce4 100644 --- a/src/dataGroups/CamtAccount.ts +++ b/src/dataGroups/CamtAccount.ts @@ -2,22 +2,19 @@ import { AlphaNumeric } from '../dataElements/AlphaNumeric.js'; import { DataGroup } from './DataGroup.js'; export type CamtAccount = { - iban?: string; - bic?: string; + iban?: string; + bic?: string; }; export class CamtAccountGroup extends DataGroup { - constructor(name: string, minCount = 0, maxCount = 1, minVersion?: number, maxVersion?: number) { - super( - name, - [ - new AlphaNumeric('iban', 0, 1, 34), - new AlphaNumeric('bic', 0, 1, 11), - ], - minCount, - maxCount, - minVersion, - maxVersion, - ); - } + constructor(name: string, minCount = 0, maxCount = 1, minVersion?: number, maxVersion?: number) { + super( + name, + [new AlphaNumeric('iban', 0, 1, 34), new AlphaNumeric('bic', 0, 1, 11)], + minCount, + maxCount, + minVersion, + maxVersion, + ); + } } diff --git a/src/interactions/statementInteractionCAMT.ts b/src/interactions/statementInteractionCAMT.ts index d2a85e9..749bfe2 100644 --- a/src/interactions/statementInteractionCAMT.ts +++ b/src/interactions/statementInteractionCAMT.ts @@ -53,13 +53,12 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction { // Parse all CAMT messages (one per booking day) and combine statements const allStatements: Statement[] = []; for (const camtMessage of hicaz.bookedTransactions) { + // camtMessage is initially encoded as 'latin1' (ISO-8859-1), but actually contains UTF-8 data. + // Therefore, we need to first convert it back to a buffer using 'latin1', and then decode it as 'utf8'. + const intermediateBuffer = Buffer.from(camtMessage, 'latin1'); + const utf8String = intermediateBuffer.toString('utf8'); - // camtMessage is initially encoded as 'latin1' (ISO-8859-1), but actually contains UTF-8 data. - // Therefore, we need to first convert it back to a buffer using 'latin1', and then decode it as 'utf8'. - const intermediateBuffer = Buffer.from(camtMessage, 'latin1'); - const utf8String = intermediateBuffer.toString('utf8'); - - const parser = new CamtParser(utf8String); + const parser = new CamtParser(utf8String); const statements = parser.parse(); allStatements.push(...statements); } diff --git a/src/segments/HKCAZ.ts b/src/segments/HKCAZ.ts index 916bbf7..e1ac302 100644 --- a/src/segments/HKCAZ.ts +++ b/src/segments/HKCAZ.ts @@ -3,10 +3,10 @@ import { Dat } from '../dataElements/Dat.js'; import { Numeric } from '../dataElements/Numeric.js'; import { Text } from '../dataElements/Text.js'; import { YesNo } from '../dataElements/YesNo.js'; +import { type CamtAccount, CamtAccountGroup } from '../dataGroups/CamtAccount.js'; import { DataGroup } from '../dataGroups/DataGroup.js'; import type { SegmentWithContinuationMark } from '../segment.js'; import { SegmentDefinition } from '../segmentDefinition.js'; -import {CamtAccount, CamtAccountGroup} from "../dataGroups/CamtAccount.js"; export type HKCAZSegment = SegmentWithContinuationMark & { account: CamtAccount; diff --git a/src/tests/camtParser.test.ts b/src/tests/camtParser.test.ts index 22d0775..cd2d481 100644 --- a/src/tests/camtParser.test.ts +++ b/src/tests/camtParser.test.ts @@ -729,8 +729,8 @@ describe('CamtParser', () => { expect(transaction.amount).toBe(200.0); }); - it('should handle multiple entries in RmtInf (Ustrd)', () => { - const camtXml = ` + it('should handle multiple entries in RmtInf (Ustrd)', () => { + const camtXml = ` @@ -878,56 +878,56 @@ describe('CamtParser', () => { `; - const parser = new CamtParser(camtXml); - const statements = parser.parse(); - - expect(statements).toHaveLength(1); - const statement = statements[0]; - expect(statement.transactions).toHaveLength(1); - - const transaction = statement.transactions[0]; - - // Check all Transaction fields filled by the parser - expect(transaction.amount).toBe(-179.46); - expect(transaction.customerReference).toBe('VG 2025 QUARTAL IV'); - expect(transaction.bankReference).toBe('TXN003'); - expect(transaction.purpose).toBe( - '28,65EUR EREF: VG 2025 QUARTAL IV IBAN\n: DE12345678901234567891 BIC: BANKABC1XXX', - ); - expect(transaction.remoteName).toBe('ABC Bank'); - expect(transaction.remoteAccountNumber).toBe('DE12345678901234567891'); - expect(transaction.remoteBankId).toBe('BANKABC1XXX'); - expect(transaction.e2eReference).toBe('VG 2025 QUARTAL IV'); - - // Check date fields - expect(transaction.valueDate).toBeInstanceOf(Date); - expect(transaction.valueDate.getFullYear()).toBe(2026); - expect(transaction.valueDate.getMonth()).toBe(0); // November (0-based) - expect(transaction.valueDate.getDate()).toBe(5); - expect(transaction.entryDate).toBeInstanceOf(Date); - expect(transaction.entryDate.getFullYear()).toBe(2026); - expect(transaction.entryDate.getMonth()).toBe(0); // November (0-based) - expect(transaction.entryDate.getDate()).toBe(5); - - // Check transaction type and code fields - expect(transaction.fundsCode).toBe('PMNT'); - expect(transaction.transactionType).toBe('ICDT'); - expect(transaction.transactionCode).toBe('ESCT'); - - // Check additional information fields - expect(transaction.additionalInformation).toBe('ENTGELT gem. Vereinbarung'); - expect(transaction.bookingText).toBe('ENTGELT gem. Vereinbarung'); // Should match additionalInformation - - // Verify optional fields not set in this test - expect(transaction.primeNotesNr).toBeUndefined(); - expect(transaction.remoteIdentifier).toBeUndefined(); - expect(transaction.client).toBeUndefined(); - expect(transaction.textKeyExtension).toBeUndefined(); - }); - - it('should handle full iso date time in value date', () => { - // this is an example from comdirect bank in 2026-01 - const camtXml = ` + const parser = new CamtParser(camtXml); + const statements = parser.parse(); + + expect(statements).toHaveLength(1); + const statement = statements[0]; + expect(statement.transactions).toHaveLength(1); + + const transaction = statement.transactions[0]; + + // Check all Transaction fields filled by the parser + expect(transaction.amount).toBe(-179.46); + expect(transaction.customerReference).toBe('VG 2025 QUARTAL IV'); + expect(transaction.bankReference).toBe('TXN003'); + expect(transaction.purpose).toBe( + '28,65EUR EREF: VG 2025 QUARTAL IV IBAN\n: DE12345678901234567891 BIC: BANKABC1XXX', + ); + expect(transaction.remoteName).toBe('ABC Bank'); + expect(transaction.remoteAccountNumber).toBe('DE12345678901234567891'); + expect(transaction.remoteBankId).toBe('BANKABC1XXX'); + expect(transaction.e2eReference).toBe('VG 2025 QUARTAL IV'); + + // Check date fields + expect(transaction.valueDate).toBeInstanceOf(Date); + expect(transaction.valueDate.getFullYear()).toBe(2026); + expect(transaction.valueDate.getMonth()).toBe(0); // November (0-based) + expect(transaction.valueDate.getDate()).toBe(5); + expect(transaction.entryDate).toBeInstanceOf(Date); + expect(transaction.entryDate.getFullYear()).toBe(2026); + expect(transaction.entryDate.getMonth()).toBe(0); // November (0-based) + expect(transaction.entryDate.getDate()).toBe(5); + + // Check transaction type and code fields + expect(transaction.fundsCode).toBe('PMNT'); + expect(transaction.transactionType).toBe('ICDT'); + expect(transaction.transactionCode).toBe('ESCT'); + + // Check additional information fields + expect(transaction.additionalInformation).toBe('ENTGELT gem. Vereinbarung'); + expect(transaction.bookingText).toBe('ENTGELT gem. Vereinbarung'); // Should match additionalInformation + + // Verify optional fields not set in this test + expect(transaction.primeNotesNr).toBeUndefined(); + expect(transaction.remoteIdentifier).toBeUndefined(); + expect(transaction.client).toBeUndefined(); + expect(transaction.textKeyExtension).toBeUndefined(); + }); + + it('should handle full iso date time in value date', () => { + // this is an example from comdirect bank in 2026-01 + const camtXml = ` @@ -1019,50 +1019,50 @@ describe('CamtParser', () => { `; - const parser = new CamtParser(camtXml); - const statements = parser.parse(); - - expect(statements).toHaveLength(1); - const statement = statements[0]; - expect(statement.transactions).toHaveLength(1); - - const transaction = statement.transactions[0]; - - // Check all Transaction fields filled by the parser - expect(transaction.amount).toBe(-101.5); - expect(transaction.customerReference).toBe(''); - expect(transaction.bankReference).toBe('5J2C21XL0470L56V/39761'); - expect(transaction.purpose).toBe( - '028-1234567-XXXXXXX Amazon.de 2ABCD\nEF9GFP28\nEnd-to-End-Ref.:\n2ABCDEF9GHIJKL28\nCORE / Mandatsref.:\n7829857lkklag\nGläubiger-ID:\nDE24ABC00000123456', - ); - expect(transaction.remoteName).toBe('AMAZON EU S.A R.L., NIEDERL ASSUNG DEUTSCHLAND'); - expect(transaction.remoteAccountNumber).toBe(''); - expect(transaction.remoteBankId).toBe(''); - expect(transaction.e2eReference).toBe(''); - - // Check date fields - expect(transaction.valueDate).toBeInstanceOf(Date); - expect(transaction.valueDate.getFullYear()).toBe(2025); - expect(transaction.valueDate.getMonth()).toBe(11); // November (0-based) - expect(transaction.valueDate.getDate()).toBe(10); - expect(transaction.entryDate).toBeInstanceOf(Date); - expect(transaction.entryDate.getFullYear()).toBe(2025); - expect(transaction.entryDate.getMonth()).toBe(11); // November (0-based) - expect(transaction.entryDate.getDate()).toBe(10); - - // Check transaction type and code fields - expect(transaction.fundsCode).toBe('DBIT'); - expect(transaction.transactionType).toBe(''); - expect(transaction.transactionCode).toBe(''); - - // Check additional information fields - expect(transaction.additionalInformation).toBe(''); - expect(transaction.bookingText).toBe(''); // Should match additionalInformation - - // Verify optional fields not set in this test - expect(transaction.primeNotesNr).toBeUndefined(); - expect(transaction.remoteIdentifier).toBeUndefined(); - expect(transaction.client).toBeUndefined(); - expect(transaction.textKeyExtension).toBeUndefined(); - }); + const parser = new CamtParser(camtXml); + const statements = parser.parse(); + + expect(statements).toHaveLength(1); + const statement = statements[0]; + expect(statement.transactions).toHaveLength(1); + + const transaction = statement.transactions[0]; + + // Check all Transaction fields filled by the parser + expect(transaction.amount).toBe(-101.5); + expect(transaction.customerReference).toBe(''); + expect(transaction.bankReference).toBe('5J2C21XL0470L56V/39761'); + expect(transaction.purpose).toBe( + '028-1234567-XXXXXXX Amazon.de 2ABCD\nEF9GFP28\nEnd-to-End-Ref.:\n2ABCDEF9GHIJKL28\nCORE / Mandatsref.:\n7829857lkklag\nGläubiger-ID:\nDE24ABC00000123456', + ); + expect(transaction.remoteName).toBe('AMAZON EU S.A R.L., NIEDERL ASSUNG DEUTSCHLAND'); + expect(transaction.remoteAccountNumber).toBe(''); + expect(transaction.remoteBankId).toBe(''); + expect(transaction.e2eReference).toBe(''); + + // Check date fields + expect(transaction.valueDate).toBeInstanceOf(Date); + expect(transaction.valueDate.getFullYear()).toBe(2025); + expect(transaction.valueDate.getMonth()).toBe(11); // November (0-based) + expect(transaction.valueDate.getDate()).toBe(10); + expect(transaction.entryDate).toBeInstanceOf(Date); + expect(transaction.entryDate.getFullYear()).toBe(2025); + expect(transaction.entryDate.getMonth()).toBe(11); // November (0-based) + expect(transaction.entryDate.getDate()).toBe(10); + + // Check transaction type and code fields + expect(transaction.fundsCode).toBe('DBIT'); + expect(transaction.transactionType).toBe(''); + expect(transaction.transactionCode).toBe(''); + + // Check additional information fields + expect(transaction.additionalInformation).toBe(''); + expect(transaction.bookingText).toBe(''); // Should match additionalInformation + + // Verify optional fields not set in this test + expect(transaction.primeNotesNr).toBeUndefined(); + expect(transaction.remoteIdentifier).toBeUndefined(); + expect(transaction.client).toBeUndefined(); + expect(transaction.textKeyExtension).toBeUndefined(); + }); }); From 6554b9948b50bafdf28106ff65a449fc2d12cc0a Mon Sep 17 00:00:00 2001 From: Anton Schegg Date: Thu, 22 Jan 2026 16:11:26 +0100 Subject: [PATCH 5/7] Improve test of date by using getUTCDate --- src/tests/camtParser.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/tests/camtParser.test.ts b/src/tests/camtParser.test.ts index cd2d481..ba270fe 100644 --- a/src/tests/camtParser.test.ts +++ b/src/tests/camtParser.test.ts @@ -979,10 +979,10 @@ describe('CamtParser', () => { DBIT BOOK -
2025-12-10+01:00
+
2025-12-08-01:00
- 2025-12-10T00:00:00.000+01:00 + 2025-12-10T00:00:00.000-01:00 5J2C21XL0470L56V/39761 @@ -1041,14 +1041,14 @@ describe('CamtParser', () => { expect(transaction.e2eReference).toBe(''); // Check date fields - expect(transaction.valueDate).toBeInstanceOf(Date); - expect(transaction.valueDate.getFullYear()).toBe(2025); - expect(transaction.valueDate.getMonth()).toBe(11); // November (0-based) - expect(transaction.valueDate.getDate()).toBe(10); + expect(transaction.valueDate).toBeInstanceOf(Date); + expect(transaction.valueDate.getFullYear()).toBe(2025); + expect(transaction.valueDate.getMonth()).toBe(11); // November (0-based) + expect(transaction.valueDate.getUTCDate()).toBe(10); expect(transaction.entryDate).toBeInstanceOf(Date); expect(transaction.entryDate.getFullYear()).toBe(2025); expect(transaction.entryDate.getMonth()).toBe(11); // November (0-based) - expect(transaction.entryDate.getDate()).toBe(10); + expect(transaction.entryDate.getUTCDate()).toBe(8); // Check transaction type and code fields expect(transaction.fundsCode).toBe('DBIT'); From be6910b3a1136af9bc0dd670042392a3840c3889 Mon Sep 17 00:00:00 2001 From: Anton Schegg Date: Thu, 22 Jan 2026 16:13:34 +0100 Subject: [PATCH 6/7] Fix biome linter issues --- src/tests/camtParser.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/camtParser.test.ts b/src/tests/camtParser.test.ts index ba270fe..42ded05 100644 --- a/src/tests/camtParser.test.ts +++ b/src/tests/camtParser.test.ts @@ -1041,10 +1041,10 @@ describe('CamtParser', () => { expect(transaction.e2eReference).toBe(''); // Check date fields - expect(transaction.valueDate).toBeInstanceOf(Date); - expect(transaction.valueDate.getFullYear()).toBe(2025); - expect(transaction.valueDate.getMonth()).toBe(11); // November (0-based) - expect(transaction.valueDate.getUTCDate()).toBe(10); + expect(transaction.valueDate).toBeInstanceOf(Date); + expect(transaction.valueDate.getFullYear()).toBe(2025); + expect(transaction.valueDate.getMonth()).toBe(11); // November (0-based) + expect(transaction.valueDate.getUTCDate()).toBe(10); expect(transaction.entryDate).toBeInstanceOf(Date); expect(transaction.entryDate.getFullYear()).toBe(2025); expect(transaction.entryDate.getMonth()).toBe(11); // November (0-based) From 4ee7274b676abbe3a8f1e4861afc1ce8c9be09ae Mon Sep 17 00:00:00 2001 From: Anton Schegg Date: Thu, 22 Jan 2026 17:05:36 +0100 Subject: [PATCH 7/7] If xml encoding is utf-8, convert it accordingly --- src/interactions/statementInteractionCAMT.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/interactions/statementInteractionCAMT.ts b/src/interactions/statementInteractionCAMT.ts index 749bfe2..f0f0bfa 100644 --- a/src/interactions/statementInteractionCAMT.ts +++ b/src/interactions/statementInteractionCAMT.ts @@ -53,12 +53,20 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction { // Parse all CAMT messages (one per booking day) and combine statements const allStatements: Statement[] = []; for (const camtMessage of hicaz.bookedTransactions) { - // camtMessage is initially encoded as 'latin1' (ISO-8859-1), but actually contains UTF-8 data. - // Therefore, we need to first convert it back to a buffer using 'latin1', and then decode it as 'utf8'. - const intermediateBuffer = Buffer.from(camtMessage, 'latin1'); - const utf8String = intermediateBuffer.toString('utf8'); + // The regex looks for the XML declaration `` + // and checks if it contains the attribute encoding="UTF-8". + // The 'i' flag makes the match case-insensitive (e.g., for "utf-8"). + const isUtf8Encoded = /<\?xml[^>]*encoding="UTF-8"[^>]*\?>/i.test(camtMessage); - const parser = new CamtParser(utf8String); + let xmlString: string = camtMessage; + if (isUtf8Encoded) { + // camtMessage is initially encoded as 'latin1' (ISO-8859-1), but actually contains UTF-8 data. + // Therefore, we need to first convert it back to a buffer using 'latin1', and then decode it as 'utf8'. + const intermediateBuffer = Buffer.from(camtMessage, 'latin1'); + xmlString = intermediateBuffer.toString('utf8'); + } + + const parser = new CamtParser(xmlString); const statements = parser.parse(); allStatements.push(...statements); }