From 624d831bdafeca937af4fd92fbf4d6377fd03fff Mon Sep 17 00:00:00 2001 From: phkoenig Date: Tue, 28 Jul 2026 20:57:30 +0200 Subject: [PATCH 1/2] fix: deliver every portion of a response the bank spreads over messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A response too large for one message is announced with return code 3040 and a continuation mark. Repeating the order yields the rest. Two defects made that data vanish without a trace — `success: true`, zero transactions, no warning. 1. Delivery. `handlePartedMessages` reassigned its `responseMessage` parameter to the follow-up message and spliced the assembled segment into THAT one. The caller kept the first message, which still held the unresolved PARTED segment, so `findSegment` found nothing. 2. Assembly. The portions were concatenated as raw bytes, assuming one segment continues mid-field. It does not: each portion is a COMPLETE response segment. A follow-up HICAZ repeats account and CAMT descriptor before carrying its own share of the documents, so gluing the raw text produced an unparseable segment ("Extra text at the end" from the CAMT parser). Every portion is now decoded on its own and all of them are placed into the message the caller holds. Combining the payloads requires knowing what they mean — one MT940 stream continues, a list of CAMT documents is appended — so that step moved to the interactions, which collect via `findAllSegments`: CAMT and SEPA accounts append their lists, MT940 and MT535 join their streams, credit card statements append their transactions. Measured against a live bank (Berliner Volksbank, HKCAZ v1) on a credit card account whose volume exceeds one message: 75 days 0 -> 233 transactions 60 days 0 -> 206 transactions 40 days 105 unchanged (fits one message, never affected) The two returned CAMT documents cover adjacent, non-overlapping periods, so nothing is counted twice. Regression tests cover the parted and the unparted case plus the interaction; there was no test for code 3040 before, which is why this could go unnoticed. Co-Authored-By: Claude Opus 5 --- src/dialog.ts | 91 +++++----- .../creditcardStatementInteraction.ts | 11 +- src/interactions/portfolioInteraction.ts | 14 +- src/interactions/sepaAccountInteraction.ts | 8 +- src/interactions/statementInteractionCAMT.ts | 12 +- src/interactions/statementInteractionMT940.ts | 14 +- src/tests/partedResponse.test.ts | 161 ++++++++++++++++++ 7 files changed, 259 insertions(+), 52 deletions(-) create mode 100644 src/tests/partedResponse.test.ts diff --git a/src/dialog.ts b/src/dialog.ts index 99d54ab..af8ee2c 100644 --- a/src/dialog.ts +++ b/src/dialog.ts @@ -264,56 +264,71 @@ export class Dialog { return this.config.tanMediaName; } + /** + * Collects a response that the bank spreads over several messages. + * + * When the bank cannot fit a response into one message it answers with code 3040 plus + * a continuation mark. Repeating the order with that mark yields the next portion — + * as a COMPLETE, self-contained response segment, not as a byte-wise continuation of + * the previous one. A HICAZ follow-up, for example, repeats the account and the CAMT + * descriptor before carrying its own share of the statements. + * + * Every portion is therefore decoded on its own and all of them are placed into the + * response message the caller holds. Combining their payloads needs to know what the + * payload means — one MT940 stream continues, a list of CAMT documents is appended — + * so that step belongs to the interaction, which does it via `findAllSegments`. + */ private async handlePartedMessages( message: CustomerMessage, responseMessage: Message, interaction: CustomerInteraction, ) { - let partedSegment = responseMessage.findSegment(PARTED.Id); - - if (partedSegment) { - while (responseMessage.hasReturnCode(3040)) { - const answers = responseMessage.getBankAnswers(); - const segmentWithContinuation = message.segments.find( - (s) => s.header.segId === interaction.segId, - ) as SegmentWithContinuationMark; - if (!segmentWithContinuation) { - throw new Error( - `Response contains segment with further information, but corresponding segment could not be found or is not specified`, - ); - } + const partedSegment = responseMessage.findSegment(PARTED.Id); - const answer = answers.find((a) => a.code === 3040); + if (!partedSegment) { + return; + } - if (!answer || !answer.params || answer.params.length === 0) { - throw new Error( - 'Expected bank answer to contain continuation mark parameters (code 3040)', - ); - } + // The message the caller holds — every portion has to end up in THIS one, not in + // the last one we happen to receive. + const callersMessage = responseMessage; + const rawPortions = [partedSegment.rawData]; + + while (responseMessage.hasReturnCode(3040)) { + const answers = responseMessage.getBankAnswers(); + const segmentWithContinuation = message.segments.find( + (s) => s.header.segId === interaction.segId, + ) as SegmentWithContinuationMark; + if (!segmentWithContinuation) { + throw new Error( + `Response contains segment with further information, but corresponding segment could not be found or is not specified`, + ); + } - segmentWithContinuation.continuationMark = answer.params[0]; - const hnhbkSegment = message.findSegment(HNHBK.Id); - if (!hnhbkSegment) { - throw new Error('HNHBK segment not found in message'); - } - hnhbkSegment.msgNr = ++this.lastMessageNumber; - const nextResponseMessage = await this.httpClient.sendMessage(message); - const nextPartedSegment = nextResponseMessage.findSegment(PARTED.Id); - - if (nextPartedSegment) { - nextPartedSegment.rawData = - partedSegment.rawData + - nextPartedSegment.rawData.slice(nextPartedSegment.rawData.indexOf('+') + 1); - partedSegment = nextPartedSegment; - } + const answer = answers.find((a) => a.code === 3040); + + if (!answer || !answer.params || answer.params.length === 0) { + throw new Error('Expected bank answer to contain continuation mark parameters (code 3040)'); + } - responseMessage = nextResponseMessage; + segmentWithContinuation.continuationMark = answer.params[0]; + const hnhbkSegment = message.findSegment(HNHBK.Id); + if (!hnhbkSegment) { + throw new Error('HNHBK segment not found in message'); } + hnhbkSegment.msgNr = ++this.lastMessageNumber; + const nextResponseMessage = await this.httpClient.sendMessage(message); + const nextPartedSegment = nextResponseMessage.findSegment(PARTED.Id); - const completeSegment = decode(partedSegment.rawData); - const index = responseMessage.segments.indexOf(partedSegment); - responseMessage.segments.splice(index, 1, completeSegment); + if (nextPartedSegment) { + rawPortions.push(nextPartedSegment.rawData); + } + + responseMessage = nextResponseMessage; } + + const index = callersMessage.segments.indexOf(partedSegment); + callersMessage.segments.splice(index, 1, ...rawPortions.map((raw) => decode(raw))); } private checkEnded(response: ClientResponse) { diff --git a/src/interactions/creditcardStatementInteraction.ts b/src/interactions/creditcardStatementInteraction.ts index b8d122c..2a26b72 100644 --- a/src/interactions/creditcardStatementInteraction.ts +++ b/src/interactions/creditcardStatementInteraction.ts @@ -49,7 +49,16 @@ export class CreditCardStatementInteraction extends CustomerOrderInteraction { return parseFloat(valueFloatStr); } - const dikku = response.findSegment(DIKKU.Id); + // A response the bank spread over several messages arrives as several DIKKU + // segments. The balance is the same in each, the transactions are not. + const dikkuSegments = response.findAllSegments(DIKKU.Id); + const dikku = dikkuSegments[0] + ? { + ...dikkuSegments[0], + transactions: dikkuSegments.flatMap((segment) => segment.transactions ?? []), + } + : undefined; + if (dikku) { const creditDebit = dikku.balance.creditDebit; const balanceAmount = dikku.balance.amount.value * (creditDebit === 'D' ? -1 : 1); diff --git a/src/interactions/portfolioInteraction.ts b/src/interactions/portfolioInteraction.ts index ce74a13..50fbe65 100644 --- a/src/interactions/portfolioInteraction.ts +++ b/src/interactions/portfolioInteraction.ts @@ -72,17 +72,23 @@ export class PortfolioInteraction extends CustomerOrderInteraction { } handleResponse(response: Message, clientResponse: PortfolioResponse): void { - const hiwpdSegment = response.findSegment(HIWPD.Id); + // A response the bank spread over several messages arrives as several HIWPD + // segments carrying one continuous MT535 stream, so their payloads are joined. + const portfolioStatement = response + .findAllSegments(HIWPD.Id) + .map((segment) => segment.portfolioStatement) + .filter((statement) => !!statement) + .join(''); - if (hiwpdSegment?.portfolioStatement) { + if (portfolioStatement) { try { // Parse the MT535 data - const parser = new Mt535Parser(hiwpdSegment.portfolioStatement); + const parser = new Mt535Parser(portfolioStatement); clientResponse.portfolioStatement = parser.parse(); } catch (error) { console.warn('Failed to parse MT535 portfolio statement:', error); // Fallback: provide raw data if parsing fails - clientResponse.rawMT535Data = hiwpdSegment.portfolioStatement; + clientResponse.rawMT535Data = portfolioStatement; } } } diff --git a/src/interactions/sepaAccountInteraction.ts b/src/interactions/sepaAccountInteraction.ts index 26bd133..224c342 100644 --- a/src/interactions/sepaAccountInteraction.ts +++ b/src/interactions/sepaAccountInteraction.ts @@ -43,9 +43,11 @@ export class SepaAccountInteraction extends CustomerOrderInteraction { } handleResponse(response: Message, clientResponse: SepaAccountResponse) { - const hispa = response.findSegment(HISPA.Id); - if (hispa) { - clientResponse.sepaAccounts = hispa.sepaAccounts || []; + // A response the bank spread over several messages arrives as several HISPA + // segments, each carrying its own share of the accounts. + const hispaSegments = response.findAllSegments(HISPA.Id); + if (hispaSegments.length > 0) { + clientResponse.sepaAccounts = hispaSegments.flatMap((segment) => segment.sepaAccounts ?? []); this.dialog?.config.bankingInformation.upd?.bankAccounts.forEach((bankAccount) => { bankAccount.isSepaAccount = false; diff --git a/src/interactions/statementInteractionCAMT.ts b/src/interactions/statementInteractionCAMT.ts index f0f0bfa..435cea0 100644 --- a/src/interactions/statementInteractionCAMT.ts +++ b/src/interactions/statementInteractionCAMT.ts @@ -47,12 +47,18 @@ export class StatementInteractionCAMT extends CustomerOrderInteraction { } handleResponse(response: Message, clientResponse: StatementResponse) { - const hicaz = response.findSegment(HICAZ.Id); - if (hicaz?.bookedTransactions && hicaz.bookedTransactions.length > 0) { + // A response the bank spread over several messages arrives as several HICAZ + // segments, each carrying its own share of the CAMT documents. Taking only the + // first one would silently drop everything after it. + const camtMessages = response + .findAllSegments(HICAZ.Id) + .flatMap((segment) => segment.bookedTransactions ?? []); + + if (camtMessages.length > 0) { try { // Parse all CAMT messages (one per booking day) and combine statements const allStatements: Statement[] = []; - for (const camtMessage of hicaz.bookedTransactions) { + for (const camtMessage of camtMessages) { // 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"). diff --git a/src/interactions/statementInteractionMT940.ts b/src/interactions/statementInteractionMT940.ts index 9209361..dadaf54 100644 --- a/src/interactions/statementInteractionMT940.ts +++ b/src/interactions/statementInteractionMT940.ts @@ -36,10 +36,18 @@ export class StatementInteractionMT940 extends CustomerOrderInteraction { } handleResponse(response: Message, clientResponse: StatementResponse) { - const hikaz = response.findSegment(HIKAZ.Id); - if (hikaz?.bookedTransactions) { + // A response the bank spread over several messages arrives as several HIKAZ + // segments. Unlike CAMT these carry one continuous MT940 stream, so their + // payloads are joined rather than listed. + const bookedTransactions = response + .findAllSegments(HIKAZ.Id) + .map((segment) => segment.bookedTransactions) + .filter((booked) => !!booked) + .join(''); + + if (bookedTransactions) { try { - const parser = new Mt940Parser(hikaz.bookedTransactions); + const parser = new Mt940Parser(bookedTransactions); clientResponse.statements = parser.parse(); } catch (error) { console.warn('MT940 parsing failed:', error); diff --git a/src/tests/partedResponse.test.ts b/src/tests/partedResponse.test.ts new file mode 100644 index 0000000..2c2def3 --- /dev/null +++ b/src/tests/partedResponse.test.ts @@ -0,0 +1,161 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { BankingInformation } from '../bankingInformation.js'; +import type { BankTransaction } from '../bankTransaction.js'; +import { FinTSConfig } from '../config.js'; +import { Dialog } from '../dialog.js'; +import { StatementInteractionCAMT } from '../interactions/statementInteractionCAMT.js'; +import { CustomerOrderMessage, Message } from '../message.js'; +import { HICAZ, type HICAZSegment } from '../segments/HICAZ.js'; +import { HKCAZ, type HKCAZSegment } from '../segments/HKCAZ.js'; +import { registerSegments } from '../segments/registry.js'; + +vi.mock('../httpClient.js', () => ({ + HttpClient: class MockHttpClient { + constructor( + public url: string, + public debug = false, + public debugRaw = false, + ) {} + sendMessage = vi.fn(); + }, +})); + +registerSegments(); + +const CAMT_DESCRIPTOR = 'urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08'; + +/** + * A HICAZ segment as the bank sends it. Every portion of a parted response is a + * COMPLETE segment — it repeats account and descriptor before carrying its own share + * of the CAMT documents. + */ +function hicazText(...camtDocuments: string[]): string { + const booked = camtDocuments.map((doc) => `@${doc.length}@${doc}`).join(':'); + return `HICAZ:5:1+DE991234567123456:BANK12+${CAMT_DESCRIPTOR}+${booked}'`; +} + +function responseMessage(hicaz: string, withContinuation: boolean): Message { + const answers = withContinuation + ? "HIRMG:3:2+0010::Entgegengenommen.+3040::Es liegen weitere Umsaetze vor.:AUFSETZ_1'" + : "HIRMG:3:2+0010::Entgegengenommen.+0020::Abfrage erfolgreich.'"; + return Message.decode(`${answers}${hicaz}`, HICAZ.Id); +} + +describe('parted responses (bank answer code 3040)', () => { + let config: FinTSConfig; + let dialog: Dialog; + + beforeEach(() => { + const bankingInformation: BankingInformation = { + systemId: 'MOCK_SYSTEM_ID', + bankMessages: [], + bpd: { + version: 1, + bankId: '12030000', + bankName: 'Mock Bank', + countryCode: 280, + url: 'http://mock.bank.url', + allowedTransactions: [ + { transId: 'HKCAZ', tanRequired: false, versions: [1] } as BankTransaction, + ], + supportedTanMethods: [], + availableTanMethodIds: [], + maxTransactionsPerMessage: 1, + supportedLanguages: [], + supportedHbciVersions: [300], + }, + } as unknown as BankingInformation; + + config = FinTSConfig.fromBankingInformation( + 'PRODUCT', + '1.0', + bankingInformation, + 'user', + 'pin', + ); + dialog = new Dialog(config); + }); + + it('delivers every portion into the message the caller holds', async () => { + const first = responseMessage(hicazText('one'), true); + const second = responseMessage(hicazText('two', 'three'), false); + + vi.mocked(dialog.httpClient.sendMessage).mockResolvedValueOnce(second); + + const interaction = new StatementInteractionCAMT('123'); + const request = new CustomerOrderMessage(HKCAZ.Id, HICAZ.Id); + request.addSegment({ + header: { segId: HKCAZ.Id, segNr: 0, version: 1 }, + account: { iban: 'DE991234567123456', bic: 'BANK12' }, + acceptedCamtFormats: ['urn:iso:std:iso:20022:tech:xsd:camt.052.001.08'], + allAccounts: false, + } as HKCAZSegment); + + // biome-ignore lint/suspicious/noExplicitAny: reaching into the private collector on purpose + await (dialog as any).handlePartedMessages(request, first, interaction); + + // Before the fix this was a single unresolved PARTED segment and everything after + // the first portion was lost without a trace. + const segments = first.findAllSegments(HICAZ.Id); + expect(first.findAllSegments('PARTED')).toHaveLength(0); + expect(segments).toHaveLength(2); + expect(segments.flatMap((s) => s.bookedTransactions)).toEqual([ + 'one', + 'two', + 'three', + ]); + }); + + it('leaves an unparted response untouched', async () => { + const only = responseMessage(hicazText('one'), false); + + const interaction = new StatementInteractionCAMT('123'); + const request = new CustomerOrderMessage(HKCAZ.Id, HICAZ.Id); + request.addSegment({ + header: { segId: HKCAZ.Id, segNr: 0, version: 1 }, + account: { iban: 'DE991234567123456', bic: 'BANK12' }, + acceptedCamtFormats: ['urn:iso:std:iso:20022:tech:xsd:camt.052.001.08'], + allAccounts: false, + } as HKCAZSegment); + + // biome-ignore lint/suspicious/noExplicitAny: reaching into the private collector on purpose + await (dialog as any).handlePartedMessages(request, only, interaction); + + expect(dialog.httpClient.sendMessage).not.toHaveBeenCalled(); + const segments = only.findAllSegments(HICAZ.Id); + expect(segments).toHaveLength(1); + expect(segments[0].bookedTransactions).toEqual(['one']); + }); +}); + +describe('StatementInteractionCAMT with a parted response', () => { + it('parses the CAMT documents of every segment, not just the first', () => { + const camt = (id: string, amount: string) => + `` + + `${id}2026-07-01T10:00:00+02:00` + + `${id}DE991234567123456EUR` + + `PRCD1000.00` + + `CRDT
2026-06-30
` + + `CLBD990.00` + + `CRDT
2026-07-01
` + + `${amount}DBIT` + + `
2026-07-01
2026-07-01
` + + `TXN${id}` + + `Test ${id}` + + `
`; + + const message = Message.decode( + `${hicazText(camt('A', '10.00'))}${hicazText(camt('B', '20.00'))}`, + ); + expect(message.findAllSegments(HICAZ.Id)).toHaveLength(2); + + const interaction = new StatementInteractionCAMT('123'); + const clientResponse = { statements: [] } as never; + interaction.handleResponse(message, clientResponse); + + const transactions = ( + clientResponse as unknown as { statements: { transactions: unknown[] }[] } + ).statements.flatMap((s) => s.transactions); + expect(transactions).toHaveLength(2); + }); +}); From 61b898d8ed95bd363cbc3c4d00f9d0de0bbf64de Mon Sep 17 00:00:00 2001 From: phkoenig Date: Wed, 29 Jul 2026 01:42:11 +0200 Subject: [PATCH 2/2] fix: resolve every parted portion, and stop mistaking parameter segments for responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the parted-response handling, both found by adversarial review. Only the FIRST placeholder was resolved. A bank message may carry several response segments; the rest stayed in the tree as PARTED, where findAllSegments cannot see them — lost without a trace, the same silent shape as the defect this code was written to fix. And the id comparison was a plain startsWith, so looking for HIEKA also caught HIEKAS, HICAZ caught HICAZS. A parameter segment held back as PARTED is never decoded. A segment starts with 'SEGID:number:version', so the colon belongs in the comparison. Tests cover both: two response segments in one message, and a parameter segment that must pass through untouched. Co-Authored-By: Claude Opus 5 --- src/dialog.ts | 29 ++++++++----- src/message.ts | 6 ++- src/tests/partedResponse.test.ts | 73 ++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 11 deletions(-) diff --git a/src/dialog.ts b/src/dialog.ts index af8ee2c..1ab6a63 100644 --- a/src/dialog.ts +++ b/src/dialog.ts @@ -283,16 +283,19 @@ export class Dialog { responseMessage: Message, interaction: CustomerInteraction, ) { - const partedSegment = responseMessage.findSegment(PARTED.Id); + // ALL of them, not just the first: one bank message may well carry several + // response segments. Taking only the first left the rest sitting in the tree as + // PARTED, where `findAllSegments` cannot see them — lost without a trace. + const partedSegments = responseMessage.findAllSegments(PARTED.Id); - if (!partedSegment) { + if (partedSegments.length === 0) { return; } // The message the caller holds — every portion has to end up in THIS one, not in // the last one we happen to receive. const callersMessage = responseMessage; - const rawPortions = [partedSegment.rawData]; + const rawPortions = partedSegments.map((segment) => segment.rawData); while (responseMessage.hasReturnCode(3040)) { const answers = responseMessage.getBankAnswers(); @@ -318,17 +321,23 @@ export class Dialog { } hnhbkSegment.msgNr = ++this.lastMessageNumber; const nextResponseMessage = await this.httpClient.sendMessage(message); - const nextPartedSegment = nextResponseMessage.findSegment(PARTED.Id); - - if (nextPartedSegment) { - rawPortions.push(nextPartedSegment.rawData); - } + rawPortions.push( + ...nextResponseMessage + .findAllSegments(PARTED.Id) + .map((segment) => segment.rawData), + ); responseMessage = nextResponseMessage; } - const index = callersMessage.segments.indexOf(partedSegment); - callersMessage.segments.splice(index, 1, ...rawPortions.map((raw) => decode(raw))); + // Every PARTED placeholder gives way to the decoded portions, at the position of + // the first one so the segment order stays intact. + const index = callersMessage.segments.indexOf(partedSegments[0]); + const withoutPlaceholders = callersMessage.segments.filter( + (segment) => segment.header.segId !== PARTED.Id, + ); + withoutPlaceholders.splice(index, 0, ...rawPortions.map((raw) => decode(raw))); + callersMessage.segments = withoutPlaceholders; } private checkEnded(response: ClientResponse) { diff --git a/src/message.ts b/src/message.ts index 2ede9c7..4b4dfe7 100644 --- a/src/message.ts +++ b/src/message.ts @@ -105,7 +105,11 @@ export class Message { } static decodeSegment(text: string, partedResponseSegId?: string): Segment { - if (partedResponseSegId && text.startsWith(partedResponseSegId)) { + // The colon matters: a segment starts with `SEGID:number:version`, so a plain + // `startsWith` would also catch the parameter segment whose id merely begins the + // same way — HIEKAS when looking for HIEKA, HICAZS for HICAZ. Those would then be + // held back as PARTED and never decoded. + if (partedResponseSegId && text.startsWith(`${partedResponseSegId}:`)) { const partedSegment: PartedSegment = { header: { ...(SegmentDefinition.header.decode(text, 1) as SegmentHeader), diff --git a/src/tests/partedResponse.test.ts b/src/tests/partedResponse.test.ts index 2c2def3..5b0bdd9 100644 --- a/src/tests/partedResponse.test.ts +++ b/src/tests/partedResponse.test.ts @@ -159,3 +159,76 @@ describe('StatementInteractionCAMT with a parted response', () => { expect(transactions).toHaveLength(2); }); }); + +describe('several response segments in one bank message', () => { + it('resolves every portion, not just the first', async () => { + // Eine Botschaft mit ZWEI HICAZ-Segmenten. Vorher wurde nur das erste aufgeloest; + // das zweite blieb als PARTED im Baum und war fuer findAllSegments unsichtbar. + const answers = "HIRMG:3:2+0010::Entgegengenommen.+0020::Abfrage erfolgreich.'"; + const message = Message.decode( + `${answers}${hicazText('one')}${hicazText('two')}`, + HICAZ.Id, + ); + expect(message.findAllSegments('PARTED')).toHaveLength(2); + + const dialog = new Dialog( + FinTSConfig.fromBankingInformation( + 'PRODUCT', + '1.0', + { + systemId: 'X', + bankMessages: [], + bpd: { + version: 1, + bankId: '12030000', + bankName: 'Mock', + countryCode: 280, + url: 'http://mock.bank.url', + allowedTransactions: [{ transId: 'HKCAZ', tanRequired: false, versions: [1] }], + supportedTanMethods: [], + availableTanMethodIds: [], + maxTransactionsPerMessage: 1, + supportedLanguages: [], + supportedHbciVersions: [300], + }, + // biome-ignore lint/suspicious/noExplicitAny: schlanker Mock + } as any, + 'user', + 'pin', + ), + ); + + const request = new CustomerOrderMessage(HKCAZ.Id, HICAZ.Id); + request.addSegment({ + header: { segId: HKCAZ.Id, segNr: 0, version: 1 }, + account: { iban: 'DE991234567123456', bic: 'BANK12' }, + acceptedCamtFormats: ['urn:iso:std:iso:20022:tech:xsd:camt.052.001.08'], + allAccounts: false, + } as HKCAZSegment); + + // biome-ignore lint/suspicious/noExplicitAny: private Sammelroutine, absichtlich + await (dialog as any).handlePartedMessages( + request, + message, + new StatementInteractionCAMT('123'), + ); + + expect(message.findAllSegments('PARTED')).toHaveLength(0); + const segments = message.findAllSegments(HICAZ.Id); + expect(segments).toHaveLength(2); + expect(segments.flatMap((s) => s.bookedTransactions)).toEqual([ + 'one', + 'two', + ]); + }); + + it('does not mistake a parameter segment for a response segment', () => { + // HICAZS begins like HICAZ. Without the colon in the comparison it would be held + // back as PARTED and never decoded — the same for HIEKAS/HIEKA, HIKAZS/HIKAZ. + const hicazs = + "HICAZS:16:1:4+1+1+0+450:N:N:urn?:iso?:std?:iso?:20022?:tech?:xsd?:camt.052.001.08'"; + const message = Message.decode(hicazs, HICAZ.Id); + expect(message.findAllSegments('PARTED')).toHaveLength(0); + expect(message.findAllSegments('HICAZS')).toHaveLength(1); + }); +});