fix: deliver every portion of a response the bank spreads over several messages (code 3040) - #31
Merged
robocode13 merged 2 commits intoAug 1, 2026
Conversation
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 <noreply@anthropic.com>
…nts for responses 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 <noreply@anthropic.com>
Owner
|
Thank you for this contribution. I myself didn't have this problem before even with a 1.400 transaction statement, but then every bank handles this differently i guess. At least i can confirm that everything i can test with four different banks is still working, so i will adopt this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The symptom
When a bank spreads a response over several messages, the data never reaches the caller. Not with an error — with
success: true, zero transactions and no warning:The wider the date range, the more likely it is. A caller has no way to tell this apart from an account that genuinely had no activity, which is what makes it expensive: a sync can report success for weeks while silently losing everything.
What I measured
Berliner Volksbank (GAD backend), credit card account via
HKCAZv1, same account, same day, only the range varied:The two returned CAMT documents cover adjacent, non-overlapping periods, so nothing is counted twice.
Three defects, all in the same path
1 · Delivery.
handlePartedMessagesreassigned itsresponseMessageparameter to the follow-up message and spliced the assembled segment into that one. The caller still holds the first message, which still contains the unresolvedPARTEDplaceholder — sofindSegmentfinds nothing.2 · Assembly. The portions were concatenated as raw bytes, on the assumption that a segment continues mid-field. It does not. Each portion is a complete, self-contained response segment: a follow-up
HICAZrepeats account and CAMT descriptor before carrying its own share of the documents. Gluing the raw text produced an unparseable segment — the CAMT parser reportsExtra text at the end.3 · Only the first placeholder was resolved. One bank message may carry several response segments. The rest stayed in the tree as
PARTED, invisible tofindAllSegments.Separately,
Message.decodeSegmentcompared the segment id with a plainstartsWith, so looking forHICAZalso caughtHICAZS(andHIEKAcaughtHIEKAS). A parameter segment held back asPARTEDis never decoded. A segment starts withSEGID:number:version, so the colon belongs in the comparison.What changed
Every portion is decoded on its own, and all of them are placed into the message the caller holds.
Combining the payloads needs to know what they mean, so that step moved to the interactions, which now collect via
findAllSegments:HICAZ)HISPA)DIKKU)HIKAZ)HIWPD)That distinction matters: joining a list of CAMT documents produces invalid XML, and appending MT940 fragments produces an unparseable stream.
No public API change. A response that fits into one message takes exactly the path it took before.
Tests
src/tests/partedResponse.test.ts, 5 cases: a parted response delivered to the caller's message; portions decoded individually rather than byte-glued; several response segments in one message; a parameter segment (HICAZS) passing through untouched; and the unparted case as a regression guard.117 tests pass (112 before). There was no test for code 3040 previously, which is why this could go unnoticed.
Scope of verification — please read
The live evidence above comes from one bank and one backend (Berliner Volksbank, GAD,
HKCAZv1). Everything else — MT940, MT535, SEPA, credit card viaDIKKU— follows the specification and is covered by tests, but I have not seen a real 3040 response on those paths. The per-interaction combination rules are the part I would look at hardest, since they are where a wrong assumption would show up as corrupted rather than missing data.Happy to adjust naming, comment density or structure to your preference.
Found while tracking down a 69-day gap in a production credit card sync. Written with AI assistance (noted in the commit trailers); the measurements and the review are my own.