# PLAN-K0: Card purchases spike (local Enable Banking test app) Status: the test app landed `2ce317f` (`docs/spikes/k0/app.py`, `docs/spikes/k0/README.md`). What is left is running it with the owner (and the partner) against the real SJ Prio Mastercard and writing the report. This plan describes the app as built and the remaining work (D-176). ## 1. Goal Fast feedback on the real bank before any Android, server or web work: try the Enable Banking flow on the MacBook with the owner's own SJ Prio Mastercard, see real data on the owner's own screen, and answer every K0 `TODO(spec)` of SPEC §9.12 in a report with aggregates only. The answers go into SPEC v0.36 (a docs agent) before K1 starts. ## 2. Read first 1. SPEC §9.12 (every `TODO(spec)` marked K0, CP-1 to CP-14), FR-22, §8.7 (no personal content in logs), §10.5.2 (the `/cards/callback` block), D-158 to D-176. 2. `docs/spikes/k0/README.md` and `docs/spikes/k0/app.py` (the app as built). 3. Enable Banking's official documentation (API reference, quick start, FAQ), read at run time (`CLAUDE.md` "Fast-moving APIs"). 4. `CLAUDE.md` (personal data, reports are deliverables, stay inside the repository). ## 3. Already established - The app (as built, `2ce317f`): one command, `python3 docs/spikes/k0/app.py` (standard library only; the RS256 JWT is signed by shelling out to `openssl`), serves `http://localhost:8765/` and opens the browser. Options `--port`, `--no-browser`, `--local-dir`, `--api-base`. Sections: 1 Status; 2 Banker (`GET /aspsps?country=SE`, SJ/SEB Kort/Eurocard first, `maximum_consent_validity`, `auth_methods`, `required_psu_headers`); 3 Anslut (`POST /auth`, `valid_until` = now + the ASPSP's maximum capped at 180 days less 5 minutes, `psu_type` `personal`, a random `state`, the bank in a new tab, BankID by QR); 4 Klistra in (the redirect address pasted back, `state` checked, `POST /sessions`); 5 Konton (`GET /sessions/{id}`, identifiers masked to 4 characters); 6 Hämta: "Uppdatera" (PSU headers, `date_from`, optional `strategy=longest`, `continuation_key` followed), "Bakgrundshämtning" (no PSU headers: the 4-per-day path), "Saldo"; 7 Köp (dates, text, amount, currency, direction, status, `entry_reference`, compared with the previous fetch); 8 Analys (aggregates only); 9 Koppla från (`DELETE /sessions/{id}`). History choices 7, 30, 90, 180, 365, 730 days. - Key, config and state live in `docs/spikes/k0/.local/` (git-ignored by the root `.gitignore`): `.pem`, optional `config.json`, `state.json` (settings, bank list, session id, account, the last two fetches, the aggregate fetch log). A re-run reuses the session until `valid_until`, so no new BankID. stdout shows method, path template and status only. - Enable Banking's production control panel refuses `http://localhost` redirect URLs ("uses unsupported scheme"). The registered redirect URL is `https://kvitt.suorra.duckdns.org/cards/callback`; it does not load yet (no K1 server), but the address bar keeps `code` and `state`, which the owner pastes into section 4. - Documented facts already in the spec (§9.12): the PSU header names and "either all required PSU headers … or none"; "many ASPSPs have the limit of 4 times a day for data fetches when PSU is not online"; `maximum_consent_validity` in seconds; `entry_reference` "unique and immutable", "in most cases … provided only for booked transactions"; `transaction_id` "may change"; `WRONG_TRANSACTIONS_PERIOD` and `strategy=longest`. - Real purchase data may show on the owner's screen. It never goes into git, a log, a report, a chat message or a file outside `.local/`. The application id is not a secret but stays out of every committed file. ## 4. Owned paths `docs/spikes/k0/**` (the app; `.local/` stays git-ignored and uncommitted) and `docs/spikes/K0-enable-banking.md` (new). Nothing else: not `SPEC.md`, `docs/decisions.md`, `server/**`, `infra/**`. ## 5. Deliverables - `docs/spikes/K0-enable-banking.md`, aggregates only, recording the commit it was taken at, with one answer per K0 item of §9.12 (each: what was measured, the answer, the proposed spec text): 1. The SJ Prio ASPSP's exact `name` and `country`, `auth_methods`, `psu_types`, `beta`, `required_psu_headers` (CP-1, CP-5). 2. `maximum_consent_validity` (seconds and days) and the `valid_until` the session returned (CP-6). 3. History depth: the earliest date reached for 90, 180 and 365 days asked right after BankID, and for the same asks a day later (CP-3); what `strategy=longest` returned and whether `WRONG_TRANSACTIONS_PERIOD` appeared (CP-7). 4. Background fetch without PSU headers: accepted or refused (status and error code), and how many in 24 hours before a refusal, if any (CP-4). 5. User-present fetch from the Mac's address: accepted with which headers (CP-5). 6. Accounts per session, their `cash_account_type` values, and the account-pick rule (CP-2); whether the account uid is the same after a second BankID (renewal). 7. Field presence over the fetched transactions: `entry_reference` (booked and pending), `transaction_date`, `value_date`, `booking_date`, `creditor.name`, `remittance_information`, other text fields; which field holds the shop name (CP-7 merchant rule; describe without quoting any text). 8. Pending behaviour: counts of `PDNG`, whether a pending row reappears as `BOOK` with or without the same `entry_reference`, the days between, whether amounts or texts change (CP-8); whether the ±3-day amount and first-token rule would have matched every case. 9. Currency: any non-SEK `transaction_amount.currency`; for a known foreign purchase, what the card reports (CP-7). 10. Credit rows: counts and kinds (refund, bill payment, other), described without amounts (CP-8). 11. The partner's card: whether it could be linked to the owner's application in the control panel and fetched (CP-13); if not, the exact refusal. 12. Amount format (decimal string, separator, sign) (CP-7). 13. Anything that contradicts §9.12, with the proposed revision. - A short "Results (owner's phone)" section is not needed (nothing runs on the phone), but a "Results (owner's run)" section lists each owner step with its yes/no. - Fixes to `app.py` found while running (iterate in minutes; stdlib only, at most one JWT library if openssl fails), committed with the report. ## 6. Steps 1. Worktree: from `/Users/jhag/projects/kvitt`, `git worktree add .claude/worktrees/agent-k0 -b agent-k0 main`; in it `git reset --hard main`; `tools/install-git-hooks.sh`. Copy `docs/spikes/k0/.local/` from the main checkout if the owner already put the key there (never commit it). 2. Read §2. Check the app starts: `python3 docs/spikes/k0/app.py --no-browser` and `curl -s localhost:8765/ | head` (no key yet: section 1 says so). 3. Relay the owner steps of §8 one at a time through the orchestrator; run the app for the owner (it opens the browser on the Mac). 4. After each owner step, read the aggregates only, never `state.json` itself: `python3 -c "import sys; sys.path.insert(0,'docs/spikes/k0'); import app; print(app.analysis_text(app.load_state()))"` from the worktree root (checked on an empty state, 2026-10-24). Nothing else is printed from `.local/`; where the analysis lacks a K0 item of §5, extend `analysis_text` (aggregates only) rather than reading the state. 5. A day later (owner step 9): repeat "Uppdatera" and "Bakgrundshämtning" without a new BankID to see the reused session, the history depth later and the background path over 24 hours. 6. Write the report; `git status` must list nothing under `docs/spikes/k0/.local/`. Commit on `agent-k0`, `git rebase main`. Never merge, push or remove the worktree. 7. At the end ask the owner (through the orchestrator) whether to keep the session for K1's tests or to press "9 Koppla från" (which deletes the stored transactions from `state.json`); the key stays in `.local/` for K1, which copies it to `infra/secrets/cards/` (PLAN-K1). ## 7. Gate None: K0 is not part of any gate. Before committing: `python3 -m py_compile docs/spikes/k0/app.py`; `git status --porcelain` shows no file under `.local/`; `grep` the report for anything that looks like an amount with `kr`, a date of a purchase or the application id (none may appear). Report what was run. ## 8. HUMAN steps (the orchestrator relays them in chat, one at a time, exact labels as on screen; steps already done are confirmed with the owner and skipped) 1. In a browser on the Mac open `https://enablebanking.com`, open the Control Panel and sign up with your email address; confirm the email. When Enable Banking shows its Terms of Service, read and accept them: this is the licence step Kvitt needs before using the service (answer yes or no in chat). 2. Control Panel › API applications › "Add a new application": environment **Production** (restricted production: your own accounts only), application name **Kvitt**, redirect URL exactly `https://kvitt.suorra.duckdns.org/cards/callback` (only this one; `http://localhost` is refused), keep the private key generated in the browser, fill the required contact fields, then register. Expected: the browser downloads a file `.pem` (yes/no). 3. Move that `.pem` file from Downloads into the folder `/Users/jhag/projects/kvitt/docs/spikes/k0/.local/` (create the folder `.local` if it is missing: in Finder press Cmd+Shift+. to show hidden folders). Keep the file name. Expected: the test app's section 1 shows "key found" (the agent checks). 4. Recommended before the first connection, so the one-time code in the bank's redirect never reaches NPM's log: NPM › Hosts › Proxy Hosts › the Kvitt row › Edit › Advanced, add at the end of Custom Nginx Configuration the `location = /cards/callback { … }` block of SPEC §10.5.2 (the agent pastes it in chat), Save. Expected: the row is still Online (yes/no). 5. Control Panel › the Kvitt application › "Activate by linking accounts": country Sweden, the bank listed as SJ Prio Mastercard, sign with BankID (scan the QR with the BankID app on your phone). Expected: your card account is listed as linked (yes/no). 6. With your partner: on the same page link her SJ Prio Mastercard, she signs with her own BankID. Expected: a second linked account (yes/no); if it fails, say the exact message. 7. Test app (the agent starts it, the browser opens `http://localhost:8765/`): "2 Banker" › "Lista banker", keep the highlighted SJ Prio entry, "Välj bank". "3 Anslut": Historik 90 dagar, "Anslut med BankID"; a bank tab opens, scan the QR with BankID. Expected: the tab ends on a page that does not load (yes/no). Copy the whole address from that tab's address bar, paste it in "4 Klistra in", "Använd adressen". Expected: "5 Konton" shows your card account (yes/no; how many accounts). 8. "6 Hämta" › "Uppdatera". Expected: "7 Köp" lists your recent purchases and the newest ones look right (yes/no). Then "Bakgrundshämtning". Expected: it fetches, or shows an error (say which: fetched or error). 9. The next day: open `http://localhost:8765/` again (the agent starts the app), "Uppdatera" and "Bakgrundshämtning" without a new BankID. Expected: both work without BankID (yes/no each). 10. If step 6 worked: repeat steps 7 and 8 with your partner's BankID for her card (yes/no each). ## 9. Out of scope - Anything in `server/**`, `android/**`, `web/**`, `contracts/**`, `infra/**` (K1 to K3), and NPM changes beyond the optional step 4. - Writing real data anywhere outside `docs/spikes/k0/.local/`; reading `state.json` other than through `analysis_text`. - Editing `SPEC.md` or `docs/decisions.md` (the report proposes the text). ## 10. Report - Branch `agent-k0`, head commit after rebasing; what was run (py_compile, the `git status` and grep checks). - One line per K0 item of §5 with the answer, and the proposed SPEC v0.36 text for every `TODO(spec)` of §9.12 (CP-1 to CP-8, CP-13), plus proposed decision entries numbered from the brief. - The owner steps done, with their yes/no; anything still to do, worded for relay. - Whether the session is kept for K1 and where the key is. ## 11. Numbering Proposed decisions from the brief's range; the spec version the findings go into is v0.36.