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

79 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.