Files
accounted/extensions/general/enable-banking/lib/__tests__/transaction-label.test.ts
T
MattssonandClaude Fable 5.1 477b59453f 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>
2026-09-08 00:37:23 +02:00

61 lines
3.0 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { deriveTransactionLabel } from '../transaction-label'
describe('deriveTransactionLabel', () => {
it('prefers MCC over the ISO code (most specific signal)', () => {
expect(deriveTransactionLabel({ mcc: '6011' })).toBe('Uttag')
expect(deriveTransactionLabel({ mcc: 5411 })).toBe('Inköp dagligvaror')
// MCC wins even when a (different-meaning) bank code is also present.
expect(
deriveTransactionLabel({ mcc: '6011', bankTransactionCode: 'PMNT/CCRD' }),
).toBe('Uttag')
})
it('maps ISO 20022 Domain/Family codes', () => {
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT/RCDT' })).toBe('Inbetalning')
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT/ICDT' })).toBe('Betalning')
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT/CCRD' })).toBe('Kortköp')
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT/RDDT' })).toBe('Autogiro')
})
it('parses dash- and dot-separated three-part codes (Domain-Family-SubFamily)', () => {
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT-CCRD-POSD' })).toBe('Kortköp')
expect(deriveTransactionLabel({ bankTransactionCode: 'pmnt.rcdt.esct' })).toBe('Inbetalning')
})
it('falls back to a keyword scan for proprietary code strings', () => {
expect(
deriveTransactionLabel({ proprietaryBankTransactionCode: 'INTEREST PAYMENT' }),
).toBe('Ränta')
expect(
deriveTransactionLabel({ proprietaryBankTransactionCode: 'ACCOUNT FEE' }),
).toBe('Avgift')
expect(
deriveTransactionLabel({ proprietaryBankTransactionCode: 'ATM WITHDRAWAL' }),
).toBe('Uttag')
})
it('reads the combined Swedish channel wording "Kortköp/uttag" as a card purchase, not a withdrawal', () => {
// Enable Banking's flattened code description for ordinary card purchases
// at SEB/Swedbank. The card rule must win over the UTTAG keyword.
expect(deriveTransactionLabel({ bankTransactionCode: 'Kortköp/uttag' })).toBe('Kortköp')
expect(deriveTransactionLabel({ bankTransactionCode: 'Card purchase' })).toBe('Kortköp')
// A bare withdrawal wording still labels as Uttag.
expect(deriveTransactionLabel({ bankTransactionCode: 'ATM WITHDRAWAL' })).toBe('Uttag')
expect(deriveTransactionLabel({ bankTransactionCode: 'Uttag' })).toBe('Uttag')
})
it('uses the bare PMNT domain + direction as a last generic resort', () => {
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT', isCredit: true })).toBe('Inbetalning')
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT', isCredit: false })).toBe('Betalning')
})
it('returns null when nothing is recognized (caller falls through)', () => {
expect(deriveTransactionLabel({})).toBeNull()
expect(deriveTransactionLabel({ bankTransactionCode: 'ZZZZ/QQQQ' })).toBeNull()
expect(deriveTransactionLabel({ mcc: '0000' })).toBeNull()
// Bare unknown domain without isCredit cannot be classified.
expect(deriveTransactionLabel({ bankTransactionCode: 'PMNT' })).toBeNull()
})
})