671c00d0d8
Build Kvitt images / build (push) Failing after 8m17s
Contract (SPEC §14.8): migration 0004_card_purchases.sql (server schema 4, byte-identical contract and server copies), E-36 to E-44 in contracts/openapi.yaml with the four new error codes and a 502 response, examples for every card body and error code, the match_card_purchase vectors (17 cases) with their Python reference in tools/contracts-check.py (157 checks), web/src/api/schema.d.ts regenerated; web/src/lib/vectors.test.ts lists match_card_purchase as not ported until K3 ports it (orchestrator's allowance). Server: src/cards/ with the provider registry (CP-1), the CardSource trait and its Enable Banking adapter (JWT RS256 kid = application id, /aspsps, /auth, /sessions, transactions with PSU headers for user-present fetches, CP-14 error mapping, CP-7 mapping), the windowed fetch (30-day windows newest first, split at the 100-row cap down to a day, de-duplication by provider_ref, continuation keys, WRONG_TRANSACTIONS_PERIOD ends the walk), CP-8 matching with the secondary rule, the background poller (06:45, 11:45, 16:45, 21:45, 4 per 24 hours, 2-hour skip, no backlog), the bilingual callback page (E-39), E-36 to E-38 and E-40 to E-44, link clearing in sync push step 7, attempt pruning at 04:00, the card_fetch log line with aggregates only, KVITT_EB_* configuration. Tests against a local stub (tests/cards_api/, 34 tests); the real API is never called. Infra: Pi compose mounts secrets/cards read-only and passes the two application ids; .env.example keys; dev stack stages infra/secrets/cards/; NPM block with the /cards/callback gate line and unlogged location (kvitt.conf and README step 4); README "Card purchases". Pre-commit hook bypassed (--no-verify, orchestrator's decision): the Android fast tests fail in :domain VectorFilesTest (and the full check in :data ExampleRoundTripTest) because they pin the contract's vector and example sets; matchCardPurchase and the card DTOs are K2's by design (contract-first landing; K2 lands next). Server, contracts-check and web gates are green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
575 lines
15 KiB
JSON
575 lines
15 KiB
JSON
{
|
|
"format": 1,
|
|
"function": "match_card_purchase",
|
|
"cases": [
|
|
{
|
|
"name": "no expenses",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": []
|
|
}
|
|
},
|
|
{
|
|
"name": "exact amount, same day",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "three days before and after are included",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-19",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-25",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001",
|
|
"0192e000-0000-7000-8000-000000000002"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "four days before or after are excluded",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-18",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-26",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": []
|
|
}
|
|
},
|
|
{
|
|
"name": "an amount 1 öre off is excluded",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 12899,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 12901,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000003",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-23",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000003"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "an expense linked from another purchase of the viewer is excluded",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": true
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-21",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000002"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "ordered by day difference",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-25",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-21",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000003",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000003",
|
|
"0192e000-0000-7000-8000-000000000002",
|
|
"0192e000-0000-7000-8000-000000000001"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "equal day difference: newer created_at first",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-20",
|
|
"created_at": "2026-10-20T08:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-24",
|
|
"created_at": "2026-10-24T08:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000002",
|
|
"0192e000-0000-7000-8000-000000000001"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "equal day difference and created_at: lower id first",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000007",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-23",
|
|
"created_at": "2026-10-23T08:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000004",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-21",
|
|
"created_at": "2026-10-23T08:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000004",
|
|
"0192e000-0000-7000-8000-000000000007"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "more candidates than max are cut",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-23",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000003",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-21",
|
|
"created_at": "2026-10-21T08:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000004",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-25",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001",
|
|
"0192e000-0000-7000-8000-000000000003",
|
|
"0192e000-0000-7000-8000-000000000002"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "max 1",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 5000,
|
|
"date": "2026-10-23",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 1
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "a credit purchase has no matches",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"direction": "credit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": []
|
|
}
|
|
},
|
|
{
|
|
"name": "a purchase without amount (foreign currency) has no matches",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": null,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 12900,
|
|
"date": "2026-10-22",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": []
|
|
}
|
|
},
|
|
{
|
|
"name": "the window crosses a month boundary",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 30000,
|
|
"date": "2026-11-01",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 30000,
|
|
"date": "2026-10-29",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 30000,
|
|
"date": "2026-10-28",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000003",
|
|
"amount_ore": 30000,
|
|
"date": "2026-11-04",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001",
|
|
"0192e000-0000-7000-8000-000000000003"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "the window crosses a year boundary",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 30000,
|
|
"date": "2027-01-02",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 30000,
|
|
"date": "2026-12-30",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 30000,
|
|
"date": "2026-12-29",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000003",
|
|
"amount_ore": 30000,
|
|
"date": "2027-01-05",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001",
|
|
"0192e000-0000-7000-8000-000000000003"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "a leap day inside the window",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 2500,
|
|
"date": "2028-03-01",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 2500,
|
|
"date": "2028-02-27",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 2500,
|
|
"date": "2028-02-26",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 3,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001"
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"name": "a 14-day window (other callers)",
|
|
"input": {
|
|
"purchase": {
|
|
"amount_ore": 2500,
|
|
"date": "2026-10-22",
|
|
"direction": "debit"
|
|
},
|
|
"expenses": [
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000001",
|
|
"amount_ore": 2500,
|
|
"date": "2026-10-08",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
},
|
|
{
|
|
"id": "0192e000-0000-7000-8000-000000000002",
|
|
"amount_ore": 2500,
|
|
"date": "2026-10-07",
|
|
"created_at": "2026-10-20T10:00:00.000Z",
|
|
"linked_by_viewer": false
|
|
}
|
|
],
|
|
"window_days": 14,
|
|
"max": 3
|
|
},
|
|
"expected": {
|
|
"ok": [
|
|
"0192e000-0000-7000-8000-000000000001"
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|