Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/camtParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down
23 changes: 23 additions & 0 deletions src/dataGroups/CamtAccount.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
8 changes: 7 additions & 1 deletion src/interactions/statementInteractionCAMT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 3 additions & 6 deletions src/segments/HKCAZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand Down
10 changes: 3 additions & 7 deletions src/tests/HKCAZ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'",
);
});

Expand All @@ -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);
});
Expand Down
Loading
Loading