fix(enable-banking): flatten Enable Banking's bank_transaction_code object to a string (#2398)
* fix(enable-banking): flatten Enable Banking's bank_transaction_code object to a string
Enable Banking serializes bank_transaction_code as {description, code,
sub_code}; three places declared it a string. The direct path passed the
object through, so PostgREST wrote its JSON text into
transactions.bank_transaction_code for every Enable Banking row since
2026-08-09 (6,356 rows, 78 companies) and the label/method derivation never
matched. The Connect service forwarded the same object and the wire contract
rejected it, so every connector-canary sync failed from 2026-09-03 (Capstone
support case 2026-09-07, "banksynken mot Nordea").
One rule, one place: normalizeBankTransactionCode in the connect-contract
file (code, code/sub_code, else description, else null), applied by
convertTransaction here and by Connect's normalizeBookedTransaction in the
mirrored contract. The wire schema stays z.string().nullable();
CONTRACT_VERSION bumps to 2026-09-08. A repair migration rewrites the stored
JSON text with the same rule and touches nothing else.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnbRMhwvUigizrPar5hW47
* fix(enable-banking): skip reset-source rows in the repair and read "Kortköp/uttag" as card
Skeptic findings on 4a30bb3f3:
- The repair migration would have aborted on prod: 110 of the 6,356 rows
belong to a migration-reset source company, whose transactions are
immutable by trigger (transactions_block_migration_reset_source_mutation).
Same failure as 20260903170000. Those rows are now excluded; nothing reads
the column back for an archived company.
- With the code description reaching the keyword tables as a string,
"Kortköp/uttag" (SEB/Swedbank wording for an ordinary card purchase)
matched UTTAG before KORT in both CODE_KEYWORD_METHODS and KEYWORD_LABELS,
so 256 card rows a month would have shown "Betalsätt: Uttag". Card now
precedes withdrawal in both tables (and in the Connect mirror), matching
what TRAILING_PHRASES already says about the same phrase.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnbRMhwvUigizrPar5hW47
* chore(migrations): annotate the repair as pg-test skip and state why no rattelse log is owed
coverage-gate flagged the migration because it creates a function; the only
function is a pg_temp helper dropped in the same statement batch, and a
one-shot UPDATE cannot be re-exercised after apply, so the annotation is the
honest disposition. The header also answers the Swedish compliance review:
the column is a write-once ingest projection with no reader, the underlag is
the archived raw PSD2 page (untouched), and the verifikat lives in
journal_entries, which the statement never reads or writes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnbRMhwvUigizrPar5hW47
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
fdcb7d937e
commit
477b59453f
@@ -148,6 +148,21 @@ describe('classifyTransactionMethod', () => {
|
||||
expect(r.method).toBe('swish')
|
||||
})
|
||||
|
||||
it('reads the flattened Enable Banking description "Kortköp/uttag" as card, not withdrawal', () => {
|
||||
// SEB/Swedbank send this combined channel wording as the code description
|
||||
// for ordinary card purchases; the row title carries only the merchant.
|
||||
expect(
|
||||
classifyTransactionMethod({ description: 'AIMO PARK', bankTransactionCode: 'Kortköp/uttag' }).method
|
||||
).toBe('card')
|
||||
expect(
|
||||
classifyTransactionMethod({ description: 'SPOTIFY', bankTransactionCode: 'Card purchase' }).method
|
||||
).toBe('card')
|
||||
// A bare withdrawal wording still classifies as withdrawal.
|
||||
expect(
|
||||
classifyTransactionMethod({ description: 'BANKOMAT 123', bankTransactionCode: 'ATM WITHDRAWAL' }).method
|
||||
).toBe('withdrawal')
|
||||
})
|
||||
|
||||
// ── MCC and explicit methods ─────────────────────────────────────────────
|
||||
|
||||
it('uses MCC presence as the card-rail fallback (6011 = ATM withdrawal)', () => {
|
||||
|
||||
@@ -172,8 +172,12 @@ const CODE_KEYWORD_METHODS: ReadonlyArray<readonly [RegExp, TransactionMethod]>
|
||||
[/AUTOGIRO/i, 'autogiro'],
|
||||
[/INTRST|INTEREST|RÄNTA|RANTA/i, 'interest'],
|
||||
[/\bFEE\b|CHRG|CHARGE|AVGIFT/i, 'fee'],
|
||||
[/ATM|CASH.?WDL|WITHDRAW|UTTAG/i, 'withdrawal'],
|
||||
// Card before withdrawal: Swedish banks send the combined channel wording
|
||||
// "Kortköp/uttag" as the code description for ordinary card purchases (SEB,
|
||||
// Swedbank), and TRAILING_PHRASES already reads that phrase as card. A bare
|
||||
// ATM/UTTAG code without a card marker still lands on withdrawal.
|
||||
[/\bCARD\b|KORT|\bPOS\b/i, 'card'],
|
||||
[/ATM|CASH.?WDL|WITHDRAW|UTTAG/i, 'withdrawal'],
|
||||
[/SALA|SALARY|\bLÖN\b|\bLON\b/i, 'salary'],
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user