Files
kvitt/PLAN-K0.md
T
jacobh2 b5114adc06 SPEC v0.35: card purchases via Enable Banking (D-158 to D-176), plans K0 to K3
Owner request and answers of 2026-10-24: each user connects their own SJ Prio
Mastercard through Enable Banking (restricted production, BankID); only the
card holder sees the purchases; a fourth bottom destination "Kortköp"
(revises D-048); "Anslut kort" in Settings with the card artwork; one- and
two-tap linking to household expenses; web as a later plan.

FR-22, §9.12 (CP-1 to CP-14), server schema 4 (migration 0004), Room 2,
E-36 to E-44, S-23 to S-27, W-21 to W-23, OQ-7, OQ-8. PLAN-K0 describes the
test app of 2ce317f and the remaining run and report; K1 server, K2 Android,
K3 web. HANDOFF and prompts/README updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:49:14 +02:00

12 KiB
Raw Blame History

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): <application-id>.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 <application id>.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.