Files
accounted/lib/transactions/__tests__/transaction-method.test.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

229 lines
8.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { classifyTransactionMethod } from '../transaction-method'
import { TRANSACTION_METHODS } from '@/types'
describe('classifyTransactionMethod', () => {
// ── Trailing channel phrases (real Swedbank-style PSD2 strings) ──────────
it('classifies and strips "Överföring via internet"', () => {
const r = classifyTransactionMethod({ description: 'Vercel Jul Överföring via internet' })
expect(r.method).toBe('transfer')
expect(r.displayTitle).toBe('Vercel Jul')
})
it('classifies and strips "Kortköp/uttag"', () => {
const r = classifyTransactionMethod({
description: 'ANTHROPIC* CLAUDE SUB SAN FRANCISCO Kortköp/uttag',
})
expect(r.method).toBe('card')
expect(r.displayTitle).toBe('ANTHROPIC* CLAUDE SUB SAN FRANCISCO')
})
it('classifies and strips "Bg-bet. via internet"', () => {
const r = classifyTransactionMethod({
description: 'Inbetalning skat BG 0000050501055 Bg-bet. via internet',
})
expect(r.method).toBe('bankgiro')
expect(r.displayTitle).toBe('Inbetalning skat BG 0000050501055')
})
it('classifies and strips "Europabetalning" and its fee sibling "Pris betalning"', () => {
const eu = classifyTransactionMethod({ description: '1260624917587 Europabetalning' })
expect(eu.method).toBe('international')
expect(eu.displayTitle).toBe('1260624917587')
const fee = classifyTransactionMethod({ description: '1260624917587 Pris betalning' })
expect(fee.method).toBe('fee')
expect(fee.displayTitle).toBe('1260624917587')
})
it('classifies and strips "Insättning"', () => {
const r = classifyTransactionMethod({ description: 'SWED2607270AUEOU Insättning' })
expect(r.method).toBe('deposit')
expect(r.displayTitle).toBe('SWED2607270AUEOU')
})
it('keeps a title that IS the phrase instead of emptying it', () => {
const r = classifyTransactionMethod({ description: 'Insättning' })
expect(r.method).toBe('deposit')
expect(r.displayTitle).toBe('Insättning')
})
it('prefers the longer phrase over its substring', () => {
// "överföring via internet" must win over the bare "överföring";
// "kortköp/uttag" must win over "uttag" (which never matches: no
// preceding whitespace).
const r = classifyTransactionMethod({ description: 'Lön Juli Emil Överföring via internet' })
expect(r.method).toBe('transfer')
expect(r.displayTitle).toBe('Lön Juli Emil')
})
it('does not match a phrase inside a word', () => {
// "Löneinsättning" ends in "insättning" but is salary, not deposit.
const salary = classifyTransactionMethod({ description: 'ACME AB Löneinsättning' })
expect(salary.method).toBe('salary')
expect(salary.displayTitle).toBe('ACME AB')
// "Bankavgift" (single word) is untouched by the bare "avgift" rule.
const bankfee = classifyTransactionMethod({ description: 'Bankavgift' })
expect(bankfee.method).toBeNull()
expect(bankfee.displayTitle).toBe('Bankavgift')
})
it('classifies leading "Swish till/från" without stripping the counterparty', () => {
const r = classifyTransactionMethod({ description: 'Swish till Erik Andersson' })
expect(r.method).toBe('swish')
expect(r.displayTitle).toBe('Swish till Erik Andersson')
})
it('adjective guard: classifies but never strips "Egen insättning" / "Eget uttag"', () => {
// The phrase IS the meaning when preceded by a possessive/scope adjective:
// stripping would leave a nonsense title ("Egen").
const deposit = classifyTransactionMethod({ description: 'Egen insättning' })
expect(deposit.method).toBe('deposit')
expect(deposit.displayTitle).toBe('Egen insättning')
const withdrawal = classifyTransactionMethod({ description: 'Eget uttag' })
expect(withdrawal.method).toBe('withdrawal')
expect(withdrawal.displayTitle).toBe('Eget uttag')
const transfer = classifyTransactionMethod({ description: 'Intern överföring' })
expect(transfer.method).toBe('transfer')
expect(transfer.displayTitle).toBe('Intern överföring')
})
// ── ISO 20022 / proprietary codes ────────────────────────────────────────
it('classifies from the ISO 20022 family when no phrase matches', () => {
const r = classifyTransactionMethod({
description: 'COOP KONSUM STOCKHOLM',
bankTransactionCode: 'PMNT-CCRD-POSD',
})
expect(r.method).toBe('card')
expect(r.displayTitle).toBe('COOP KONSUM STOCKHOLM')
})
it('lets the ISO subfamily refine the family default', () => {
expect(
classifyTransactionMethod({
description: 'Payroll run',
bankTransactionCode: 'PMNT/ICDT/SALA',
}).method
).toBe('salary')
expect(
classifyTransactionMethod({
description: 'Payment abroad',
bankTransactionCode: 'PMNT/ICDT/XBCT',
}).method
).toBe('international')
})
it('lets a subfamily on the second code beat a family match on the first', () => {
// The ISO code carries only DOMAIN/FAMILY while the proprietary code
// carries the full triple: the SALA refinement must still win.
const r = classifyTransactionMethod({
description: 'Payroll run',
bankTransactionCode: 'PMNT/ICDT',
proprietaryBankTransactionCode: 'PMNT-ICDT-SALA',
})
expect(r.method).toBe('salary')
})
it('prefers the bank-authored phrase over the generic ISO family', () => {
// A Bankgiro payment travels as an issued credit transfer (ICDT): the
// phrase is the more specific truth.
const r = classifyTransactionMethod({
description: 'TELIA AB Bg-betalning',
bankTransactionCode: 'PMNT/ICDT',
})
expect(r.method).toBe('bankgiro')
expect(r.displayTitle).toBe('TELIA AB')
})
it('falls back to a keyword scan over proprietary codes', () => {
const r = classifyTransactionMethod({
description: 'Some row',
proprietaryBankTransactionCode: 'SWISH_PAYMENT',
})
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)', () => {
expect(
classifyTransactionMethod({ description: 'SPOTIFY AB', mccCode: 5815 }).method
).toBe('card')
expect(
classifyTransactionMethod({ description: 'BANKOMAT 123', mccCode: 6011 }).method
).toBe('withdrawal')
})
it('lets an explicit source method beat every heuristic', () => {
const r = classifyTransactionMethod({
description: 'Stripe-utbetalning po_123 Kortköp',
explicitMethod: 'transfer',
})
expect(r.method).toBe('transfer')
// The phrase strip still applies to the title.
expect(r.displayTitle).toBe('Stripe-utbetalning po_123')
})
it('returns null method and the untouched title when nothing matches', () => {
const r = classifyTransactionMethod({ description: 'Okänd transaktion' })
expect(r.method).toBeNull()
expect(r.displayTitle).toBe('Okänd transaktion')
})
it('every classified method is part of the closed vocabulary', () => {
const samples = [
'X Överföring via internet',
'X Kortköp',
'X Bg-betalning',
'X Plusgiro',
'X Swish',
'X Autogiro',
'X E-faktura',
'X Europabetalning',
'X Insättning',
'X Uttag',
'X Lönebetalning',
'X Avgift',
'X Ränta',
]
for (const description of samples) {
const { method } = classifyTransactionMethod({ description })
expect(method).not.toBeNull()
expect(TRANSACTION_METHODS).toContain(method)
}
})
it('stripping yields a prefix of the original (dedup-bridge invariant)', () => {
const cases = [
'Vercel Jul Överföring via internet',
'TIC BG 0000005786439 Bg-bet. via internet',
'UTBETALNING Insättning',
'ANTHROPIC* CLAUDE SUB SAN FRANCISCO Kortköp/uttag',
]
for (const description of cases) {
const { displayTitle } = classifyTransactionMethod({ description })
expect(description.toLowerCase().startsWith(displayTitle.toLowerCase())).toBe(true)
}
})
})