8b620c8c6a
Build Kvitt images / build (push) Failing after 3h10m35s
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>
79 lines
12 KiB
Markdown
79 lines
12 KiB
Markdown
# 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.
|