diff --git a/src/camtParser.ts b/src/camtParser.ts index 4e286dd..9d7207e 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,13 @@ 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 +637,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 (!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; + } + } + + // 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..2519ce4 --- /dev/null +++ b/src/dataGroups/CamtAccount.ts @@ -0,0 +1,20 @@ +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/interactions/statementInteractionCAMT.ts b/src/interactions/statementInteractionCAMT.ts index a6dcb67..f0f0bfa 100644 --- a/src/interactions/statementInteractionCAMT.ts +++ b/src/interactions/statementInteractionCAMT.ts @@ -53,7 +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) { - const parser = new CamtParser(camtMessage); + // 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); + + 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); } diff --git a/src/segments/HKCAZ.ts b/src/segments/HKCAZ.ts index cc15151..e1ac302 100644 --- a/src/segments/HKCAZ.ts +++ b/src/segments/HKCAZ.ts @@ -3,16 +3,13 @@ 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 InternationalAccount, - InternationalAccountGroup, -} from '../dataGroups/InternationalAccount.js'; import type { SegmentWithContinuationMark } from '../segment.js'; import { SegmentDefinition } from '../segmentDefinition.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..42ded05 100644 --- a/src/tests/camtParser.test.ts +++ b/src/tests/camtParser.test.ts @@ -892,7 +892,7 @@ describe('CamtParser', () => { 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', + '28,65EUR EREF: VG 2025 QUARTAL IV IBAN\n: DE12345678901234567891 BIC: BANKABC1XXX', ); expect(transaction.remoteName).toBe('ABC Bank'); expect(transaction.remoteAccountNumber).toBe('DE12345678901234567891'); @@ -924,4 +924,145 @@ describe('CamtParser', () => { 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-08-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.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.getUTCDate()).toBe(8); + + // 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(); + }); });