Files
kvitt/PLAN-K0.md
T
jacobh2 8b620c8c6a
Build Kvitt images / build (push) Failing after 3h10m35s
K0 report, SPEC v0.36: K0 results, windowed history fetch (D-177 to D-180), card artwork cropped
K0 ran on the owner's SJ Prio Mastercard on 2026-10-24; the report
docs/spikes/K0-enable-banking.md holds aggregates only. SPEC v0.36 answers
every K0 TODO(spec) of §9.12: D-177 K0's outcome and the owner's
assumptions (partner card, pending->booked, next-day session; verified by
K1's owner steps), D-178 the windowed history fetch (100-row cap, no
continuation key: 30-day windows split at 100 rows), D-179 the transaction
mapping (merchant from remittance_information, booking date else value
date, FEE debits to handle, debtor/CPAN fields never stored), D-180 the
artwork cropped to the card (1313 x 829) and committed for Android and the
web. K plans, prompts/README and HANDOFF follow; K1 to K3 spec text goes
into v0.37.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:31:47 +02:00

12 KiB
Raw Blame History

PLAN-K0: Card purchases spike (local Enable Banking test app)

Status: done (2026-10-24). The test app landed 2ce317f (docs/spikes/k0/app.py, docs/spikes/k0/README.md); it was run with the owner on his SJ Prio Mastercard on 2026-10-24, and the report is docs/spikes/K0-enable-banking.md. Its answers are recorded in SPEC v0.36 (D-177 to D-180). Owner steps 6, 9 and 10 of §8 (the partner's card, the next day) were not run: the owner decided to assume they work (D-177), and K1's owner steps verify them on the real server (PLAN-K1 §8). The rest of this plan is kept as the record of how K0 was run (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.