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
84 lines
4.4 KiB
SQL
84 lines
4.4 KiB
SQL
-- Kvitt server schema v4: card purchases (SPEC §9.12, FR-22, D-167). Do not edit; add a new migration and bump the
|
|
-- schema version (SPEC §14.8). Applied on top of 0003_device_version_name.sql.
|
|
--
|
|
-- Card data is private to the card holder (D-161): none of these rows is in the household sync stream (GET /sync), the
|
|
-- audit log or a push. Card rows carry card_version from their own sequence and are read through GET /cards/sync.
|
|
|
|
CREATE TABLE card_sequence (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
value INTEGER NOT NULL
|
|
) STRICT;
|
|
INSERT INTO card_sequence (id, value) VALUES (1, 0);
|
|
|
|
-- One connection per user and card provider. session_id is an Enable Banking session: a credential usable only with the
|
|
-- application's private key, which is never in the database or a backup (D-161). Erased when disconnected.
|
|
CREATE TABLE card_connections (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
user_id TEXT NOT NULL REFERENCES users (id),
|
|
provider_id TEXT NOT NULL CHECK (length(provider_id) BETWEEN 1 AND 64),
|
|
status TEXT NOT NULL CHECK (status IN ('active', 'expired', 'disconnected')),
|
|
session_id TEXT,
|
|
account_uid TEXT,
|
|
valid_until TEXT,
|
|
history_days INTEGER NOT NULL CHECK (history_days BETWEEN 1 AND 365),
|
|
last_fetch_at TEXT,
|
|
last_fetch_error TEXT,
|
|
last_user_fetch_at TEXT,
|
|
background_fetches TEXT NOT NULL DEFAULT '[]',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
card_version INTEGER NOT NULL UNIQUE,
|
|
CHECK ((status = 'disconnected') = (session_id IS NULL))
|
|
) STRICT;
|
|
CREATE UNIQUE INDEX card_connections_user_provider ON card_connections (user_id, provider_id);
|
|
|
|
-- Single-use authorisation attempts (D-162); state is stored only as its SHA-256 hash. Pruned 7 days after creation.
|
|
CREATE TABLE card_auth_attempts (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
user_id TEXT NOT NULL REFERENCES users (id),
|
|
device_id TEXT NOT NULL REFERENCES devices (id),
|
|
provider_id TEXT NOT NULL CHECK (length(provider_id) BETWEEN 1 AND 64),
|
|
client TEXT NOT NULL CHECK (client IN ('android', 'web')),
|
|
state_hash TEXT NOT NULL UNIQUE,
|
|
history_days INTEGER NOT NULL CHECK (history_days BETWEEN 1 AND 365),
|
|
status TEXT NOT NULL CHECK (status IN ('pending', 'connected', 'failed', 'expired')),
|
|
failure TEXT,
|
|
connection_id TEXT REFERENCES card_connections (id),
|
|
created_at TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
finished_at TEXT
|
|
) STRICT;
|
|
CREATE INDEX card_auth_attempts_created ON card_auth_attempts (created_at);
|
|
|
|
-- Purchases as the bank reports them, mapped per SPEC §9.12 CP-7. amount_ore is null only for a non-SEK purchase
|
|
-- (original_amount and original_currency then set) and for a tombstone. A tombstone has its content erased (D-167).
|
|
CREATE TABLE card_purchases (
|
|
id TEXT PRIMARY KEY NOT NULL,
|
|
user_id TEXT NOT NULL REFERENCES users (id),
|
|
connection_id TEXT NOT NULL REFERENCES card_connections (id),
|
|
provider_ref TEXT NOT NULL,
|
|
status TEXT NOT NULL CHECK (status IN ('pending', 'booked')),
|
|
direction TEXT NOT NULL CHECK (direction IN ('debit', 'credit')),
|
|
purchase_date TEXT NOT NULL,
|
|
booking_date TEXT,
|
|
amount_ore INTEGER CHECK (amount_ore IS NULL OR amount_ore >= 1),
|
|
original_amount TEXT,
|
|
original_currency TEXT CHECK (original_currency IS NULL OR length(original_currency) = 3),
|
|
merchant TEXT NOT NULL CHECK (length(merchant) <= 140),
|
|
expense_id TEXT REFERENCES expenses (id),
|
|
ignored INTEGER NOT NULL DEFAULT 0 CHECK (ignored IN (0, 1)),
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
deleted_at TEXT,
|
|
card_version INTEGER NOT NULL UNIQUE,
|
|
CHECK (NOT (ignored = 1 AND expense_id IS NOT NULL)),
|
|
CHECK (deleted_at IS NOT NULL OR (amount_ore IS NULL) = (original_currency IS NOT NULL))
|
|
) STRICT;
|
|
CREATE UNIQUE INDEX card_purchases_ref ON card_purchases (connection_id, provider_ref) WHERE deleted_at IS NULL;
|
|
CREATE UNIQUE INDEX card_purchases_expense_once ON card_purchases (user_id, expense_id)
|
|
WHERE expense_id IS NOT NULL AND deleted_at IS NULL;
|
|
CREATE INDEX card_purchases_user_version ON card_purchases (user_id, card_version);
|
|
CREATE INDEX card_purchases_expense ON card_purchases (expense_id);
|
|
|
|
UPDATE meta SET value = '4' WHERE key = 'schema_version';
|