-- 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';