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

9.5 KiB
Raw Blame History

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)
  • 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).