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>
9.5 KiB
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_typeCARD. access.valid_untilasked for now + 180 days − 5 minutes (the ASPSP's maximum is 180 days, equal to the cap); the session returnedvalid_until2027-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_PERIODwas 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_referencegave 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_rateempty). The card account reports foreign purchases already converted to SEK. - Status:
BOOK98,PDNG2. Direction:DBIT98,CRDT2. - Credits: both
CRDTrows werePAYMENTrows, 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).