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/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);
}
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..22d0775 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();
+ });
});