Files
kvitt/docs/spikes/K0-enable-banking.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

129 lines
9.5 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.
# K0: Enable Banking and the SJ Prio Mastercard
Date: 2026-10-24. Plan `PLAN-K0.md` (D-176). Tool: the local test app `docs/spikes/k0/app.py` as committed in `2ce317f`, run on the owner's MacBook against Enable Banking's production API in restricted mode, with the owner's own SJ Prio Mastercard and his BankID. The application's key, the session and every fetched transaction stayed in the git-ignored `docs/spikes/k0/.local/`. This report holds aggregates only: no names, merchant texts, amounts, purchase dates, identifiers, the application id or the card number. Recorded in SPEC v0.36 (D-177 to D-180).
## 1. What was tested
| Step | What | Result |
|---|---|---|
| Account and licence | Enable Banking sign-up, Terms of Service accepted by the owner (the "Licences first" step) | done |
| Application | Restricted production application, redirect URL `https://kvitt.suorra.duckdns.org/cards/callback` | registered; the key file downloaded and moved to `.local/` |
| Redirect URL | `http://localhost:8765/callback` in the production control panel | refused: "uses unsupported scheme" |
| Activation | Control panel › "Activate by linking accounts", the owner's card, BankID | worked; the application runs in restricted mode |
| Bank list | `GET /aspsps?country=SE` | 126 Swedish ASPSPs, SJ Prio among them (§2) |
| Connect | `POST /auth`, BankID by QR, the redirect address pasted back into the app, `POST /sessions` | worked (the callback page does not load yet, as expected; the address bar kept `code` and `state`) |
| Session | `GET /sessions/{id}` | 1 account (§3) |
| User-present fetch | transactions with `Psu-Ip-Address` and `Psu-User-Agent` | HTTP 200 |
| Background fetch | transactions without any PSU header | HTTP 200 |
| History depth | 7, 30, 90 and 180 days back; five consecutive 30-day windows | §4 |
| Fields | presence and shape over a 100-row fetch | §5 |
Not run (owner's decision, 2026-10-24: "let's just assume it all works now given your test"): linking the partner's card and fetching it (PLAN-K0 §8 steps 6 and 10), and the next-day repeat (step 9). §7 lists what is assumed instead and where it is verified.
## 2. The ASPSP
| Field | Value |
|---|---|
| `name` | `SJ Prio Mastercard` (exactly) |
| `country` | `SE` |
| `beta` | false |
| `maximum_consent_validity` | 15552000 s (180 days) |
| `psu_types` | [`personal`] |
| auth method | `REDIRECT` |
| `required_psu_headers` | [`psu-ip-address`] (lower case as listed) |
## 3. Session and consent
* One account in the session, currency SEK, `cash_account_type` `CARD`.
* `access.valid_until` asked for now + 180 days − 5 minutes (the ASPSP's maximum is 180 days, equal to the cap); the session returned `valid_until` 2027-04-22.
* User-present fetch (`Psu-Ip-Address`, `Psu-User-Agent`) and background fetch (no PSU header): both HTTP 200. Only one background fetch was made, so the number accepted per 24 hours is not measured.
## 4. History and the 100-row cap
Every transactions response held at most 100 rows and never a `continuation_key`.
| Request | Rows | Note |
|---|---|---|
| 7 days, default strategy | 12 | |
| 30 days, default strategy | 52 | |
| 90 days, default strategy | 100 | covers only the newest 60 days: older rows are cut |
| 180 days, `strategy=longest` | 100 | neither the newest 100 nor a contiguous range: a mixed selection |
| five consecutive 30-day windows, `date_from` and `date_to`, 150 days back | 67, 85, 55, 49, 52 | all HTTP 200, no continuation key, 308 distinct rows in total |
The five-window series asked with both `date_from` and `date_to`; the committed app's form sends only `date_from`.
Findings:
* A range longer than the card's 100-row cap loses rows silently: no error, no continuation key. The fix is fetching in windows (D-178): at most 30 days each, and a window that returns 100 rows is split in half and fetched again.
* At least 150 days of history were reachable right after BankID (the oldest window answered HTTP 200 with rows). The 180-day choice stays.
* No `WRONG_TRANSACTIONS_PERIOD` was seen.
* Rows returned for a window can have booking dates one or two days outside the requested window (the bank seems to filter on another date), so windows overlap in what they return. De-duplication across windows by `entry_reference` gave the 308 distinct rows.
## 5. Transaction fields (100-row sample)
| Field | Presence | Notes |
|---|---|---|
| `entry_reference` | 100 % | also on both pending rows; all unique; unchanged between two fetches |
| `transaction_id` | 100 % | not used (Enable Banking: "may change") |
| `booking_date` | 98 % | missing exactly on the pending rows |
| `value_date` | 100 % | |
| `transaction_date` | 0 % | |
| `remittance_information` | 100 % | exactly one line on every row; it is the merchant text |
| `creditor.name` | 0 % | |
| `creditor.postal_address.country` | 100 % | SE 79, DE 14, IE 5, EE 2 |
| `creditor.postal_address.town_name` | 97 % | |
| `debtor.name` | 96 % | one distinct value: the card holder |
| `debtor_account.other.identification` | 97 % | `scheme_name` `CPAN`; one distinct value, a masked card number |
| `bank_transaction_code.code` | 100 % | `PURCHASE` 92, `PAYMENT` 5, `UNKNOWN` 2, `FEE` 1 |
| `merchant_category_code`, `exchange_rate`, `balance_after_transaction`, `note`, `reference_number` | 0 % | |
* Amounts: positive decimal strings with a point; the sign comes from `credit_debit_indicator`.
* Currency: every row `SEK`, purchases at German, Irish and Estonian merchants included (`exchange_rate` empty). The card account reports foreign purchases already converted to SEK.
* Status: `BOOK` 98, `PDNG` 2. Direction: `DBIT` 98, `CRDT` 2.
* Credits: both `CRDT` rows were `PAYMENT` rows, payments of the card bill. No refund in the sample.
## 6. Answers to the K0 `TODO(spec)` items (SPEC v0.35 §9.12)
| Item | Answer | SPEC v0.36 |
|---|---|---|
| CP-1 ASPSP name and country | `SJ Prio Mastercard`, `SE` (measured) | the registry entry uses them |
| CP-2 account pick with several accounts | the session has 1 account (`CARD`), so the rule is untested | the default rule stays: the first `CARD` account, else the first |
| CP-3 history depth right after BankID | at least 150 days (measured with windows) | 7, 30, 90, 180 stay; default 30 |
| CP-4 background fetch without PSU headers | accepted, HTTP 200 (one fetch) | the 4-per-24-hours rule stays as written |
| CP-5 `required_psu_headers` and acceptance | [`psu-ip-address`]; a fetch with `Psu-Ip-Address` and `Psu-User-Agent` was accepted (from the MacBook) | send those two, compare names case-insensitively; from the Pi it is checked by K1's owner steps |
| CP-6 `maximum_consent_validity` | 15552000 s = 180 days; the session ran to 2027-04-22 | `valid_until` = now + 180 days − 5 min |
| CP-7 `strategy=longest` | 100 rows, a mixed selection; the cap is 100 rows per response with no continuation key | windowed fetch (D-178); `strategy` is never sent |
| CP-7 dates | `booking_date` on booked rows, `value_date` on all, `transaction_date` never | `purchase_date` = `booking_date`, else `value_date` |
| CP-7 foreign purchases in SEK | yes | the non-SEK path stays as a safeguard |
| CP-7 merchant field | `remittance_information` (one line); `creditor.name` never present | merchant = `remittance_information`, else `creditor.name`, else "Kortköp" |
| CP-8 pending → booked | `entry_reference` is present on pending rows; the change to booked was not observed | owner's assumption: the reference is kept; the amount-and-date rule stays as the secondary rule |
| CP-8 credits | 2 of 100, both card-bill payments | stored, shown only under "Alla", no action (unchanged) |
| CP-13 partner's card on the owner's application | not tried | owner's assumption: yes; the per-user application (D-175) stays as the fallback |
Further rules from the field survey (D-179): `bank_transaction_code` is not used and the direction alone decides, so a debit `FEE` row is a purchase to handle (the holder turns it into an expense or ignores it). The holder's name, the masked card number (`CPAN`) and the merchant's address are never stored, logged or shown.
## 7. Assumed, not measured (owner's decision, 2026-10-24)
| Assumption | Why it is reasonable | Verified by |
|---|---|---|
| The partner's card can be linked to the owner's application and fetched | restricted mode serves the accounts linked in the application's control panel; linking is per account with the account holder's BankID | PLAN-K1 §8 step 5 (with the fallback of D-175 if refused) |
| A pending row keeps its `entry_reference` when it is booked | the reference is present on pending rows and stable between fetches | PLAN-K1 §8 step 7 (the `card_fetch` counters and the owner's yes/no) |
| The session is reused the next day without BankID | the consent runs to 2027-04-22 | PLAN-K1 §8 step 6 |
## 8. Results (owner's run)
| Owner step (PLAN-K0 §8) | Result |
|---|---|
| 1 Sign up, accept the Terms of Service | yes |
| 2 Production application with the callback URL; the key downloaded | yes |
| 3 Key moved into `docs/spikes/k0/.local/` | yes |
| 4 NPM `/cards/callback` block (optional) | not recorded; it is part of K1's NPM step |
| 5 "Activate by linking accounts" with the owner's card | yes |
| 6 The partner's card | not run (assumed, §7) |
| 7 Connect with BankID, paste the address, the card account shown | yes, 1 account |
| 8 "Uppdatera" and "Bakgrundshämtning" | both fetched (HTTP 200) |
| 9 The next day without BankID | not run (assumed, §7) |
| 10 The partner's run | not run (assumed, §7) |
The key file stays only in the main checkout's git-ignored `docs/spikes/k0/.local/`; K1's owner steps copy it to the Pi (`~/kvitt/secrets/cards/`, D-175).