K0 report, SPEC v0.36: K0 results, windowed history fetch (D-177 to D-180), card artwork cropped
Build Kvitt images / build (push) Failing after 3h10m35s
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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Kvitt: specification
|
||||
|
||||
Version **0.35** (2026-10-24). Schema version **4**. Sync protocol version **1**. Push payload version **1**.
|
||||
Version **0.36** (2026-10-24). Schema version **4**. Sync protocol version **1**. Push payload version **1**.
|
||||
|
||||
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
|
||||
|
||||
@@ -676,7 +676,7 @@ Card purchases (D-161, D-163) add: the Enable Banking application's private key
|
||||
* Nginx Proxy Manager's access log (`proxy-host-<id>_access.log` inside NPM) records the full `$request_uri`, query string included, on every path except `/i/`; the `location ^~ /i/` of the Custom Nginx Configuration has `access_log off` and `error_log /dev/null crit`, so neither the invite code nor the inviter's name (`?from=`) reaches a log, a 502 included. NPM logs no request headers, so the `Authorization` token is never logged. The API's other query parameters (`since`, `limit`, `size`, `v`) hold no personal content (§10.5.2, D-120, revises D-117).
|
||||
* The push sender logs one info line `push sent` per message with aggregates only: `push_type`, `devices_sent`, `devices_targeted` and the last 8 characters of each device id that got it; the per-device line is at debug (D-113).
|
||||
* FCM payloads carry ids and types only (§14.6).
|
||||
* Card purchases (D-161, D-163): server logs carry no merchant text, amount, date of a purchase, Enable Banking session id, account uid, authorisation `code` or `state`, and no Enable Banking response body; the card adapter logs one info line per fetch with aggregates only (`card_fetch`: provider id, kind `background` or `user`, the counts of new, updated and removed purchases, the HTTP status class of the bank call, and the last 8 characters of the connection id). `/cards/callback` is logged by the server under its route template only, and NPM does not log it at all (§10.5.2). The phone never logs card content (`CLAUDE.md`). Card data is never in a push, the audit log or `GET /sync`. Backups (§10.9) include the card tables in the SQLite snapshot, like every table; the private key is not in `data/` and is never backed up.
|
||||
* Card purchases (D-161, D-163): server logs carry no merchant text, amount, date of a purchase, Enable Banking session id, account uid, authorisation `code` or `state`, and no Enable Banking response body; the card adapter logs one info line per fetch with aggregates only (`card_fetch`: provider id, kind `background` or `user`, the counts of new, updated and removed purchases, the HTTP status class of the bank call, and the last 8 characters of the connection id; since v0.36 also the number of bank requests, of windows split and of windows truncated (CP-7), and of pending purchases booked under their own reference (`booked_by_ref`) and taken over by CP-8's secondary rule (`booked_by_match`), D-178). `/cards/callback` is logged by the server under its route template only, and NPM does not log it at all (§10.5.2). The phone never logs card content (`CLAUDE.md`). Card data is never in a push, the audit log or `GET /sync`. Backups (§10.9) include the card tables in the SQLite snapshot, like every table; the private key is not in `data/` and is never backed up.
|
||||
* Backups on Google Drive are not encrypted in v1; they go to the owner's own Drive folder, reachable only by his Google account. The owner decided on 2026-10-19 not to encrypt them with an rclone `crypt` remote and to keep the token's `drive` scope (security review M-1 closed as an accepted risk, §8.1, `docs/validation/security-review.md`).
|
||||
|
||||
### 8.8 Phone hardening
|
||||
@@ -820,7 +820,7 @@ All string lengths are counted in Unicode code points after trimming. One table,
|
||||
| `GET /sync` `limit` | 1 to 1000, default 500 | `invalid_request` |
|
||||
| `GET /sync` `since` | required, integer ≥ 0 | `invalid_request` |
|
||||
| picture upload | ≤ 5 MiB, ≤ 8000 × 8000 px, JPEG/PNG/WebP | `payload_too_large`, `image_invalid`, `unsupported_media_type` |
|
||||
| card `history_days` (E-37, E-41) | one of 7, 30, 90, 180 (CP-3; `TODO(spec)` K0 may trim 180); E-41 accepts only a value ≤ the current one | `validation_failed` |
|
||||
| card `history_days` (E-37, E-41) | one of 7, 30, 90, 180 (CP-3; K0 reached at least 150 days right after BankID, so 180 stays, D-177); E-41 accepts only a value ≤ the current one | `validation_failed` |
|
||||
| card purchase `merchant` (stored) | 0 to 140 code points, truncated by the server (CP-7) | none (never sent by clients) |
|
||||
| E-44 body | `expense_id` a non-deleted expense id or `null`; `ignored` bool; `ignored` true needs `expense_id` null; a `credit` purchase accepts only `expense_id` null and `ignored` false (CP-8) | `validation_failed`; an unknown or deleted `expense_id` is `not_found` with field `expense_id`; `expense_already_linked` |
|
||||
| `GET /cards/sync` `since`, `limit` | as `GET /sync`: `since` required, integer ≥ 0; `limit` 1 to 1000, default 500 | `invalid_request` |
|
||||
@@ -939,28 +939,38 @@ Evidence (D-049): a chronological replay of the household's 1 775 Splitwise expe
|
||||
|
||||
### 9.12 Card purchases (D-158 to D-176)
|
||||
|
||||
The rules of FR-22. The card holder is the user who connected the card; "purchase" is one card transaction as the bank reports it. Facts about the provider (established 2026-10-24, D-159): the SJ Prio Mastercard is issued by SEB Kort Bank AB and, as a payment account, is reachable through PSD2 account information (AIS): read only, no payments. Kvitt reaches it through Enable Banking (enablebanking.com), whose changelog (August 2024) lists an "SJ Prio Mastercard (SE)" integration, in its restricted production mode: free, for evaluation and individual non-commercial use, fetching data only from accounts linked to the application in Enable Banking's control panel ("Activate by linking accounts", done with the bank's own authorisation, BankID). PSD2 RTS Art. 36(5)(b): without the user actively requesting it, a provider may access an account at most 4 times per 24 hours. Since the 2023 RTS amendment, access through a provider needs a renewed strong customer authentication (BankID) at most every 180 days; the bank's `maximum_consent_validity` may be lower. History older than about 90 days is usually available only right after authentication. Items marked `TODO(spec)` K0 are measured by the spike (PLAN-K0, report `docs/spikes/K0-enable-banking.md`) and recorded in the next spec version before K1 starts; until then the stated default holds.
|
||||
The rules of FR-22. The card holder is the user who connected the card; "purchase" is one card transaction as the bank reports it. Facts about the provider (established 2026-10-24, D-159): the SJ Prio Mastercard is issued by SEB Kort Bank AB and, as a payment account, is reachable through PSD2 account information (AIS): read only, no payments. Kvitt reaches it through Enable Banking (enablebanking.com), whose changelog (August 2024) lists an "SJ Prio Mastercard (SE)" integration, in its restricted production mode: free, for evaluation and individual non-commercial use, fetching data only from accounts linked to the application in Enable Banking's control panel ("Activate by linking accounts", done with the bank's own authorisation, BankID). PSD2 RTS Art. 36(5)(b): without the user actively requesting it, a provider may access an account at most 4 times per 24 hours. Since the 2023 RTS amendment, access through a provider needs a renewed strong customer authentication (BankID) at most every 180 days; the bank's `maximum_consent_validity` may be lower. History older than about 90 days is usually available only right after authentication. The spike K0 measured the owner's card on 2026-10-24 (PLAN-K0, report `docs/spikes/K0-enable-banking.md`, D-177); its results are stated in the rules below as measured ("K0: …"). Three points were not measured and rest on the owner's decision of 2026-10-24 ("let's just assume it all works now given your test", D-177): the partner's card links to the owner's application (CP-13), a pending transaction keeps its `entry_reference` when it is booked (CP-8), and a session is reused on later days without BankID (CP-6). K1's owner steps verify all three on the real server (PLAN-K1 §8).
|
||||
|
||||
**CP-1 Catalogue (D-160).** Supported cards are entries of a provider registry in `kvitt-server` (`src/cards/providers.rs`), served by E-36. Each entry has an open-string id, a display name, the issuer, the Enable Banking ASPSP name and country, an image key and its source adapter (`enable_banking`); nothing outside the adapter names the vendor, and ids are never enums (`CLAUDE.md`). v0.35 has one entry: id `sj_prio_mastercard`, name "SJ Prio Mastercard", issuer "SEB Kort", ASPSP `TODO(spec)` K0 (default name `SJ Prio Mastercard`, country `SE`), image key `sj_prio_mastercard`. The app and the web map the image key to their bundled artwork (D-173, D-174); an unknown key draws the fallback card. A user has at most one connection per provider (D-167).
|
||||
**CP-1 Catalogue (D-160).** Supported cards are entries of a provider registry in `kvitt-server` (`src/cards/providers.rs`), served by E-36. Each entry has an open-string id, a display name, the issuer, the Enable Banking ASPSP name and country, an image key and its source adapter (`enable_banking`); nothing outside the adapter names the vendor, and ids are never enums (`CLAUDE.md`). The registry has one entry: id `sj_prio_mastercard`, name "SJ Prio Mastercard", issuer "SEB Kort", ASPSP name `SJ Prio Mastercard`, country `SE`, image key `sj_prio_mastercard` (K0: listed exactly so in `GET /aspsps?country=SE`, one of 126 Swedish ASPSPs, `beta` false, `psu_types` [`personal`], auth method `REDIRECT`). The app and the web map the image key to their bundled artwork, committed in both (D-180); an unknown key draws the fallback card. A user has at most one connection per provider (D-167).
|
||||
|
||||
**CP-2 Connecting (D-162).** E-37 creates an authorisation attempt for the caller (user and device), the provider, the chosen history and the client (`android` or `web`): a random 32-byte `state` (base64url, stored only as its SHA-256 hash, like invite codes, §8.3), status `pending`, expiry 15 minutes after creation. It calls Enable Banking `POST /auth` with `access.valid_until` (CP-6), the ASPSP, the `state`, `redirect_url` = `<KVITT_PUBLIC_URL>/cards/callback` and `psu_type` `personal`, and returns the attempt id, the bank URL and the expiry. A new attempt for the same user and provider expires a pending older one. The client opens the URL (Android: a Custom Tab, D-173; web: the same tab, D-174); the user signs with BankID (app-to-app on the phone, QR in a desktop browser). The bank redirects the browser to E-39 with `code` and `state` (or `error`). E-39 looks the attempt up by the hash of `state`; an unknown, used or expired `state`, or an `error`, marks the attempt `failed` (when known) and shows the failure page; otherwise it calls Enable Banking `POST /sessions` with the code, picks the card account (exactly one account in the session: that one; several: `TODO(spec)` K0, default the first whose `cash_account_type` is `CARD`, else the first; none: `failed` with `no_account`), creates or updates the user's connection for that provider (status `active`, the new session id, account uid, `valid_until`, history), marks the attempt `connected`, starts the first fetch in the background (CP-4, user present with the callback request's address and user agent) and shows the success page. The attempt is single use: a second callback with the same `state` shows the failure page and changes nothing. The pages (bilingual by `Accept-Language`, copy in §13.3 "Card callback page") link back to the client: `kvitt://cards/connected?attempt=<attempt id>` for `android`, `/app/cards?attempt=<attempt id>` for `web`. The page never redirects by itself (a browser may block a scripted jump to `kvitt://`); the client learns the result by polling E-38 every 2 seconds while its connect screen is open (S-25, W-22) and when the return link opens it. Renewing an existing connection (CP-6) is the same flow; on success the connection keeps its id and every purchase, and the previous session is revoked at Enable Banking (best effort).
|
||||
**CP-2 Connecting (D-162).** E-37 creates an authorisation attempt for the caller (user and device), the provider, the chosen history and the client (`android` or `web`): a random 32-byte `state` (base64url, stored only as its SHA-256 hash, like invite codes, §8.3), status `pending`, expiry 15 minutes after creation. It calls Enable Banking `POST /auth` with `access.valid_until` (CP-6), the ASPSP, the `state`, `redirect_url` = `<KVITT_PUBLIC_URL>/cards/callback` and `psu_type` `personal`, and returns the attempt id, the bank URL and the expiry. A new attempt for the same user and provider expires a pending older one. The client opens the URL (Android: a Custom Tab, D-173; web: the same tab, D-174); the user signs with BankID (app-to-app on the phone, QR in a desktop browser). The bank redirects the browser to E-39 with `code` and `state` (or `error`). E-39 looks the attempt up by the hash of `state`; an unknown, used or expired `state`, or an `error`, marks the attempt `failed` (when known) and shows the failure page; otherwise it calls Enable Banking `POST /sessions` with the code, picks the card account (exactly one account in the session: that one; several: the first whose `cash_account_type` is `CARD`, else the first; none: `failed` with `no_account`; K0: the SJ Prio session holds exactly one account, currency SEK, `cash_account_type` `CARD`), creates or updates the user's connection for that provider (status `active`, the new session id, account uid, `valid_until`, history), marks the attempt `connected`, starts the first fetch in the background (CP-4, user present with the callback request's address and user agent) and shows the success page. The attempt is single use: a second callback with the same `state` shows the failure page and changes nothing. The pages (bilingual by `Accept-Language`, copy in §13.3 "Card callback page") link back to the client: `kvitt://cards/connected?attempt=<attempt id>` for `android`, `/app/cards?attempt=<attempt id>` for `web`. The page never redirects by itself (a browser may block a scripted jump to `kvitt://`); the client learns the result by polling E-38 every 2 seconds while its connect screen is open (S-25, W-22) and when the return link opens it. Renewing an existing connection (CP-6) is the same flow; on success the connection keeps its id and every purchase, and the previous session is revoked at Enable Banking (best effort).
|
||||
|
||||
**CP-3 History (D-164).** The connect screen offers "Hämta köp från de senaste 7 / 30 / 90 / 180 dagar", default 30 days. The first fetch after an authorisation asks for transactions from today (Europe/Stockholm) minus that many days. `TODO(spec)` K0: the depth the bank gives right after authentication; default: 180 is offered; if K0 shows less, the choices are trimmed to what was reached and the default stays 30. The setting can be shortened later (E-41; it only affects later renewals, stored purchases stay); a longer value needs a fresh BankID, so S-26 offers it only through "Förnya med BankID" (E-37 with the new value), and says so ("En längre period kräver BankID igen").
|
||||
**CP-3 History (D-164).** The connect screen offers "Hämta köp från de senaste 7 / 30 / 90 / 180 dagar", default 30 days. The first fetch after an authorisation asks for transactions from today (Europe/Stockholm) minus that many days, in windows (CP-7). K0: right after BankID the card gave at least 150 days (five consecutive 30-day windows back to 150 days, all answered with rows; the last 30 days to 180 were not asked by window), so 180 stays offered (D-177). The setting can be shortened later (E-41; it only affects later renewals, stored purchases stay); a longer value needs a fresh BankID, so S-26 offers it only through "Förnya med BankID" (E-37 with the new value), and says so ("En längre period kräver BankID igen").
|
||||
|
||||
**CP-4 Background fetch (D-165).** Enable Banking's FAQ: "many ASPSPs have the limit of 4 times a day for data fetches when PSU is not online". The server fetches every `active` connection in the background at 06:45, 11:45, 16:45 and 21:45 Europe/Stockholm (four slots spread over the day, away from the 04:00 pruning and the 06:00 recurring run), without the user's headers (not user present). A slot is skipped when the connection started 4 background fetches in the preceding 24 hours (`background_fetches`, the start times of the last four), when any fetch for it succeeded less than 2 hours before, or when one is running. The scheduler reads the clock as §9.5 does; after downtime it runs at most the slot that is due, never a backlog. `TODO(spec)` K0: that a background fetch without the user's headers is accepted for the SJ Prio card (default: yes, within 4 per 24 hours).
|
||||
**CP-4 Background fetch (D-165).** Enable Banking's FAQ: "many ASPSPs have the limit of 4 times a day for data fetches when PSU is not online". The server fetches every `active` connection in the background at 06:45, 11:45, 16:45 and 21:45 Europe/Stockholm (four slots spread over the day, away from the 04:00 pruning and the 06:00 recurring run), without the user's headers (not user present). A slot is skipped when the connection started 4 background fetches in the preceding 24 hours (`background_fetches`, the start times of the last four), when any fetch for it succeeded less than 2 hours before, or when one is running. The scheduler reads the clock as §9.5 does; after downtime it runs at most the slot that is due, never a backlog. K0: a fetch without the user's headers was accepted for the SJ Prio card (HTTP 200); how many the bank accepts per 24 hours was not measured, so the 4-per-24-hours rule stands. A background fetch normally makes one bank request (its range starts 10 days before the latest stored booking date, one window of CP-7); the `card_fetch` line counts the requests (§8.7).
|
||||
|
||||
**CP-5 User-present fetch (D-165).** "Uppdatera" and pull to refresh on S-23 (and "Uppdatera" on W-21) call E-42, which fetches with the user present, so it does not count as background access. Enable Banking's PSU headers are `Psu-Ip-Address`, `Psu-User-Agent`, `Psu-Referer`, `Psu-Accept`, `Psu-Accept-Charset`, `Psu-Accept-Encoding`, `Psu-Accept-language` and `Psu-Geo-Location`, and a fetch sends "either all required PSU headers (in case of online data fetching) or none", the required set being the ASPSP's `required_psu_headers` in `GET /aspsps`. A user-present fetch therefore sends `Psu-Ip-Address` (the client IP of §8.6) and `Psu-User-Agent` always, plus every other header the ASPSP requires, each taken from the app's own request (`User-Agent`, `Referer`, `Accept`, `Accept-Charset`, `Accept-Encoding`, `Accept-Language`); `Psu-Geo-Location` is never sent (Kvitt has no location; `TODO(spec)` K0: the SJ Prio ASPSP's `required_psu_headers` and that a fetch with them is accepted from the Pi's address, default: it does not require `Psu-Geo-Location`). A background fetch sends none. E-42 contacts the bank at most once per connection per 60 seconds (§8.6) and waits up to 25 seconds for the fetch; if it takes longer it answers with `fetching` true and the fetch completes in the background. The app then pulls E-40. On the phone the regular sync triggers (AC-15.3) also pull E-40, so purchases fetched in the background reach the phone without a tap.
|
||||
**CP-5 User-present fetch (D-165).** "Uppdatera" and pull to refresh on S-23 (and "Uppdatera" on W-21) call E-42, which fetches with the user present, so it does not count as background access. Enable Banking's PSU headers are `Psu-Ip-Address`, `Psu-User-Agent`, `Psu-Referer`, `Psu-Accept`, `Psu-Accept-Charset`, `Psu-Accept-Encoding`, `Psu-Accept-language` and `Psu-Geo-Location`, and a fetch sends "either all required PSU headers (in case of online data fetching) or none", the required set being the ASPSP's `required_psu_headers` in `GET /aspsps`. A user-present fetch therefore sends `Psu-Ip-Address` (the client IP of §8.6) and `Psu-User-Agent` always, plus every other header the ASPSP requires, each taken from the app's own request (`User-Agent`, `Referer`, `Accept`, `Accept-Charset`, `Accept-Encoding`, `Accept-Language`); `Psu-Geo-Location` is never sent (Kvitt has no location). K0: the SJ Prio ASPSP's `required_psu_headers` is exactly [`psu-ip-address`]; Enable Banking lists the names in lower case, so they are compared case-insensitively; a fetch with `Psu-Ip-Address` and `Psu-User-Agent` was accepted (HTTP 200) from the MacBook, and from the Pi it is checked by K1's owner steps. A background fetch sends none. E-42 contacts the bank at most once per connection per 60 seconds (§8.6) and waits up to 25 seconds for the fetch; if it takes longer it answers with `fetching` true and the fetch completes in the background. The app then pulls E-40. On the phone the regular sync triggers (AC-15.3) also pull E-40, so purchases fetched in the background reach the phone without a tap.
|
||||
|
||||
**CP-6 Consent (D-166).** `access.valid_until` asks for now plus the smaller of 180 days and the ASPSP's `maximum_consent_validity` (given in seconds in `GET /aspsps`), less 5 minutes (`TODO(spec)` K0: the SJ Prio value, default 180 days); the stored `valid_until` is the one Enable Banking's session returns. From 14 days before it, S-23 and S-26 show the expiry notice with "Förnya med BankID"; once it has passed, or Enable Banking reports the session expired or revoked, the connection's status becomes `expired`: no fetch runs, the stored purchases stay and keep working (link, ignore), and "Förnya med BankID" renews (CP-2).
|
||||
**CP-6 Consent (D-166).** `access.valid_until` asks for now plus the smaller of 180 days and the ASPSP's `maximum_consent_validity` (given in seconds in `GET /aspsps`), less 5 minutes (K0: the SJ Prio value is 15552000 seconds, 180 days, so the request is now plus 180 days less 5 minutes; the session made on 2026-10-24 returned `valid_until` 2027-04-22); the stored `valid_until` is the one Enable Banking's session returns. The session serves every fetch, user-present and background, until then without a new BankID (owner's assumption, D-177; K0 did not repeat a fetch on a later day, K1's owner steps check it). From 14 days before it, S-23 and S-26 show the expiry notice with "Förnya med BankID"; once it has passed, or Enable Banking reports the session expired or revoked, the connection's status becomes `expired`: no fetch runs, the stored purchases stay and keep working (link, ignore), and "Förnya med BankID" renews (CP-2).
|
||||
|
||||
**CP-7 Incremental fetch and mapping (D-168).** Each fetch asks for transactions with `date_from` = the later of (today minus `history_days`) and (the latest stored booking date of the connection minus 10 days), so a purchase booked late and a pending purchase turning booked are seen again; the first fetch after an authorisation uses today minus `history_days`. Pages are followed until Enable Banking returns no continuation key. With the default strategy Enable Banking answers `WRONG_TRANSACTIONS_PERIOD` when the asked range is not available; the fetch then asks once more with `strategy=longest` and keeps the transactions from `date_from` on (`TODO(spec)` K0: what `strategy=longest` returns for the card right after BankID and later; default as written). Each transaction maps to a purchase:
|
||||
* `provider_ref`: the transaction's `entry_reference`, which Enable Banking documents as "unique and immutable" but "in most cases … provided only for booked transactions" (`transaction_id` "may change" and is not used); when absent, as for most pending transactions, `h:` + lowercase hex SHA-256 of `<booking or transaction date>|<amount>|<indicator>|<merchant text>|<n>`, `<n>` the 0-based index of an identical earlier transaction in the same response.
|
||||
* `direction`: `debit` for `DBIT`, `credit` for `CRDT`. `status`: `pending` for `PDNG`, `booked` for `BOOK`; other statuses are skipped.
|
||||
* `purchase_date`: `transaction_date`, else `value_date`, else `booking_date`; `booking_date` when booked (`TODO(spec)` K0 which dates the card reports; default as written).
|
||||
* Amount: `transaction_amount.amount` is a decimal string parsed to öre exactly (string parsing, never floating point, as §9.6's amount input); its sign is ignored, the direction carries it. `TODO(spec)` K0: foreign purchases arrive in SEK on the card account (default yes). A transaction whose currency is not `SEK` is stored with `amount_ore` null, its amount as the decimal string `original_amount` and `original_currency` (ISO 4217); such a purchase shows "<amount> <currency>" with "Utländsk valuta", gets no matches (CP-10) and "Skapa utgift" opens S-7 with the amount empty. A transaction of 0,00 is skipped.
|
||||
* `merchant`: `TODO(spec)` K0 which field holds the shop name; default: `creditor.name` when present, else the `remittance_information` lines joined with a space, else "Kortköp" ("Card purchase"); whitespace collapsed, trimmed, at most 140 code points (truncated).
|
||||
**CP-7 Incremental fetch and mapping (D-168, D-178, D-179).** Each fetch covers the range from `date_from` = the later of (today minus `history_days`) and (the latest stored booking date of the connection minus 10 days) up to today, so a purchase booked late and a pending purchase turning booked are seen again; the first fetch after an authorisation starts at today minus `history_days`. K0: the SJ Prio card answers every transactions request with at most 100 transactions and never a `continuation_key`, so a longer range loses rows without an error (90 days gave 100 rows covering only the newest 60 days; 180 days with `strategy=longest` gave 100 rows that were neither the newest nor a contiguous range). The range is therefore fetched in windows (D-178):
|
||||
* Windows: consecutive, non-overlapping, at most 30 days each, newest first, each asked with `date_from` and `date_to` (both inclusive); the oldest window takes what is left and may be shorter. A first fetch of 180 days makes 6 requests; an incremental fetch (10 to about 20 days) makes 1. A `continuation_key`, if one is ever returned, is followed within its window.
|
||||
* Split: a window whose response holds 100 or more transactions (the cap, so the response may be cut) is split into two halves, the newer half taking the extra day of an odd length, and each half is asked again, newest first, down to windows of one day. A one-day window that still holds 100 is kept as returned and counts as truncated.
|
||||
* `strategy` is never sent (Enable Banking's default strategy): `strategy=longest` neither lifts the cap nor returns the newest rows.
|
||||
* `WRONG_TRANSACTIONS_PERIOD` for a window (not seen in K0) means the bank has no older history: that window and every older one are skipped and the fetch succeeds with the newer windows.
|
||||
* A window may return transactions whose booking date lies one or two days outside it (K0: the bank filters on another date), so every transaction is kept as returned and the transactions of all windows are de-duplicated by `provider_ref` (below) before CP-8; the first occurrence, from the newer window, wins.
|
||||
* Any other error ends the fetch with CP-14's error; the transactions of the windows already fetched are applied (CP-8).
|
||||
* A fetch is complete when every window of its range answered and none was truncated; only a complete fetch deletes vanished pending purchases (CP-8).
|
||||
|
||||
**CP-8 Matching a fetch to stored purchases (D-168).** Within one connection: a transaction whose `provider_ref` is stored updates that purchase's status, dates, amount and merchant (link and ignore state kept). A booked transaction with no stored `provider_ref` (the usual case for a purchase seen pending first, since pending transactions mostly carry no `entry_reference`) takes over a stored pending purchase of the connection (its id, link and ignore state kept; its `provider_ref` replaced) when the amounts and directions are equal, the dates are at most 3 days apart and the CS-4 first tokens (§9.11) of the merchant texts are equal; among several, the oldest pending one (`TODO(spec)` K0: how pending rows turn booked; default this rule). Otherwise it is a new purchase. After a fetch, a stored pending purchase dated inside the fetched range that the bank no longer reports, neither linked nor ignored, is deleted (a tombstone with its content erased, D-167); a linked or ignored one stays as it is. Credit purchases (`credit`: refunds, deposits, the card bill's payment; `TODO(spec)` K0 what they are on this card) are stored but never "Att hantera": they show only under "Alla", marked "Insättning", with no actions, because §9.6 has no negative expense; the sheet says to change the original expense by hand.
|
||||
Each transaction maps to a purchase:
|
||||
* `provider_ref`: the transaction's `entry_reference`, which Enable Banking documents as "unique and immutable" but "in most cases … provided only for booked transactions" (`transaction_id` "may change" and is not used). K0: on the SJ Prio card it is on every transaction, the pending ones included, unique, and unchanged between two fetches. When absent, `h:` + lowercase hex SHA-256 of `<purchase_date>|<amount>|<indicator>|<merchant text>|<n>`, `<n>` the 0-based index of an identical earlier transaction in the same window's response.
|
||||
* `direction`: `debit` for `DBIT`, `credit` for `CRDT`. `status`: `pending` for `PDNG`, `booked` for `BOOK`; other statuses are skipped. `bank_transaction_code` is not used: the direction alone decides (D-179). K0 saw `PURCHASE` 92, `PAYMENT` 5, `UNKNOWN` 2 and `FEE` 1 in 100 transactions; a debit `FEE` (a card fee) or `PAYMENT` is a debit like any purchase, so it is "Att hantera" and the holder turns it into an expense or ignores it.
|
||||
* `purchase_date`: `booking_date`, else `value_date` (a pending transaction has no booking date), else `transaction_date`; a transaction with none of the three is skipped. `booking_date` is stored when the transaction is booked. K0: `value_date` on every transaction, `booking_date` on every booked one and on no pending one, `transaction_date` on none (D-179).
|
||||
* Amount: `transaction_amount.amount` is a decimal string parsed to öre exactly (string parsing, never floating point, as §9.6's amount input); its sign is ignored, the direction carries it. K0: positive decimal strings with a point, and every transaction in `SEK`, purchases at German, Irish and Estonian merchants included (`exchange_rate` empty): the card account reports foreign purchases converted to SEK. A transaction whose currency is not `SEK` (not seen in K0; kept as a safeguard) is stored with `amount_ore` null, its amount as the decimal string `original_amount` and `original_currency` (ISO 4217); such a purchase shows "<amount> <currency>" with "Utländsk valuta", gets no matches (CP-10) and "Skapa utgift" opens S-7 with the amount empty. A transaction of 0,00 is skipped.
|
||||
* `merchant`: the `remittance_information` lines joined with a space, else `creditor.name`, else "Kortköp" ("Card purchase"); whitespace collapsed, trimmed, at most 140 code points (truncated). K0: `remittance_information` holds exactly one line on every transaction and it is the merchant text; `creditor.name` is never present (D-179).
|
||||
* Nothing else of a transaction is stored, logged or sent to a client (D-179): in particular not `debtor.name` (K0: the card holder's name), `debtor_account` (K0: a masked card number, scheme `CPAN`), `creditor.postal_address` (K0: the merchant's country and town), `transaction_id` or `bank_transaction_code`.
|
||||
|
||||
**CP-8 Matching a fetch to stored purchases (D-168, D-177).** Within one connection: a transaction whose `provider_ref` is stored updates that purchase's status, dates, amount and merchant (link and ignore state kept). On the SJ Prio card a pending transaction carries an `entry_reference` (K0), and a pending transaction is assumed to keep it when it is booked (owner's assumption, D-177; K1's owner steps check it through `booked_by_ref` and `booked_by_match`, §8.7), so the booked transaction updates the pending purchase by its reference. The secondary rule, for a booked transaction with no stored `provider_ref` (a bank that gives the booked transaction a new reference, or a pending one stored under the fallback hash), takes over a stored pending purchase of the connection (its id, link and ignore state kept; its `provider_ref` replaced) when the amounts and directions are equal, the dates are at most 3 days apart and the CS-4 first tokens (§9.11) of the merchant texts are equal; among several, the oldest pending one. Otherwise it is a new purchase. After a complete fetch (CP-7), a stored pending purchase dated inside the fetched range that the bank no longer reports, neither linked nor ignored, is deleted (a tombstone with its content erased, D-167); a linked or ignored one stays as it is. Credit purchases (`credit`: refunds, deposits, the card bill's payment; K0: 2 of 100 transactions, both payments of the card bill, no refund) are stored but never "Att hantera": they show only under "Alla", marked "Insättning", with no actions, because §9.6 has no negative expense; the sheet says to change the original expense by hand.
|
||||
|
||||
**CP-9 Turning a purchase into an expense (D-169).** "Skapa utgift" opens S-7 (W-5 on the web) as a new expense prefilled with: amount = `amount_ore`; description = the merchant text, trimmed to 100 code points (§9.6), case unchanged; date = `purchase_date`; payer = the card holder (the viewer); account = from S-27 the account it was opened from, otherwise the AC-8.6 default (the account last used on this phone, or browser, if still active, else the oldest active account); category = the §9.11 suggestion for that description and account (it is a new expense, CS-1; the user's own pick wins, CS-2); split mode `default`; notes empty. Saving creates an ordinary household expense (sync push, audited, the partner is notified as for any expense) and links the purchase to its id. A purchase links to at most one expense, and an expense is linked from at most one purchase of each user (`expense_already_linked`). "Ta bort koppling" (S-23 sheet, S-8) clears the link; the expense stays. When a linked expense is deleted, by either user, the server clears the link in the same transaction (§14.4.4 step 7), so the purchase is "Att hantera" again; the phone clears it locally at its own delete. On the phone, link, unlink and ignore are written to the local copy at once (offline too) and sent with E-44 after the household push of the next sync cycle, so the expense exists on the server first (§14.4.5, D-173); the web sends E-44 at once. The link is not audited and not pushed; editing the expense later leaves the link.
|
||||
|
||||
@@ -970,7 +980,7 @@ The rules of FR-22. The card holder is the user who connected the card; "purchas
|
||||
|
||||
**CP-12 Disconnecting (D-167).** E-43 revokes the session at Enable Banking (`DELETE /sessions/{id}`, best effort: a failure is logged without the id and the disconnect goes on), sets the connection's status to `disconnected` and erases its session id and account uid, and deletes the connection's purchases that are neither linked nor ignored (tombstones with their content erased). Linked and ignored purchases stay (the user's decisions, which keep a later reconnect of the same card from offering them again) unless `erase_handled` is true, when they are deleted too. Expenses created from purchases stay. Connecting the same card again reuses the connection (one per user and provider).
|
||||
|
||||
**CP-13 Server configuration (D-175).** Enable Banking's production control panel accepts only `https` redirect URLs (an `http://localhost` URL "uses unsupported scheme"); the application's registered redirect URL is `https://<KVITT_PUBLIC_HOST>/cards/callback`, the path of E-39, and no other is needed. The application id is not a secret but is kept out of the repository's text (it is the configuration's value, §10.4, and the key file's name in K0's git-ignored folder). Card features need an Enable Banking application: an application id and its private key file (§10.4). One application may serve both users (the owner links both cards to it in Enable Banking's control panel) or each user has their own: `TODO(spec)` K0 whether the partner's card can be linked to the owner's application (default: yes; the fallback is the partner's own application, which `KVITT_EB_MEMBER_APP_ID` and its key file configure without code change). A user without a configured application sees the provider as not available (E-36 `available` false, AC-22.1); E-37 answers `cards_unavailable`. The owner accepts Enable Banking's Terms of Service before an application exists (the licence step of `CLAUDE.md`, PLAN-K0).
|
||||
**CP-13 Server configuration (D-175).** Enable Banking's production control panel accepts only `https` redirect URLs (an `http://localhost` URL "uses unsupported scheme"); the application's registered redirect URL is `https://<KVITT_PUBLIC_HOST>/cards/callback`, the path of E-39, and no other is needed. The application id is not a secret but is kept out of the repository's text (it is the configuration's value, §10.4, and the key file's name in K0's git-ignored folder). Card features need an Enable Banking application: an application id and its private key file (§10.4). One application may serve both users (the owner links both cards to it in Enable Banking's control panel) or each user has their own. The owner's application exists (K0: restricted production, activated by linking the owner's card, its key file in K0's git-ignored folder until K1's owner steps copy it to the Pi). The partner's card is assumed to link to the owner's application (owner's assumption, D-177; K0 did not try it, K1's owner steps do); the fallback is the partner's own application, which `KVITT_EB_MEMBER_APP_ID` and its key file configure without code change. A user without a configured application sees the provider as not available (E-36 `available` false, AC-22.1); E-37 answers `cards_unavailable`. The owner accepts Enable Banking's Terms of Service before an application exists (the licence step of `CLAUDE.md`, PLAN-K0).
|
||||
|
||||
**CP-14 Errors (D-165).** A fetch that fails records `last_fetch_error` on the connection: `bank_unavailable` (Enable Banking or the bank did not answer, or answered 5xx), `rate_limited` (the bank refused for too many requests), `consent_expired` (CP-6) or `failed` (anything else); a later successful fetch clears it. S-23 shows "Kunde inte hämta köp. Senast uppdaterad <time>" while it is set, except for `consent_expired`, which shows the expired state.
|
||||
|
||||
@@ -1415,9 +1425,9 @@ SPEC v0.28 ─┬─► W0 web foundation (web/**) ─────────
|
||||
└─► W7 server: /app/ serving, /analytics/* (D-135..D-138, v0.29) ─┬─► W4's W-11
|
||||
└─► W6 Android Insights
|
||||
|
||||
Card purchases (v0.35, D-158 to D-176):
|
||||
Card purchases (v0.35, D-158 to D-176; K0 done, v0.36; K1 to K3 reports recorded in v0.37):
|
||||
SPEC v0.35 ─► owner steps (Enable Banking account, ToS, application, key, linked cards) ─► K0 local test app on the Mac
|
||||
─► K0 report ─► SPEC v0.36 (K0's TODO(spec) answers, docs agent) ─┬─► K1 server (schema 4, adapter, E-36..E-44, poller, NPM text) ──┐ lands first
|
||||
─► K0 report ─► SPEC v0.36 (K0's answers, D-177 to D-180) ────────┬─► K1 server (schema 4, adapter, E-36..E-44, poller, NPM text) ──┐ lands first
|
||||
├─► K2 Android (:feature:cards, Room 2, Kortköp tab) ──────────┼─► lands after K1
|
||||
└─► K3 web (W-21..W-23) ────────────────────────────────────────┘ lands after K2 (card strings)
|
||||
```
|
||||
@@ -1465,10 +1475,10 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
| W4 | Web: analytics and insights | Landed `1aecf51` (2026-10-21, D-151, D-152, SPEC v0.32). W-9, W-10 with the D-128 line chart, W-11 on the D-133 endpoints (E-34, E-35); `resolveRange` and `spendSeries` passing their vectors (`features/analytics/domain/analytics.vectors.test.ts`); in `web/src/features/analytics/` | Web gate | Look at the analytics pages in a browser (yes/no) | W0; W-11 needs W7 |
|
||||
| W5 | Web: settings | Landed `3ebe57d` (2026-10-21, D-153, D-154, SPEC v0.32). W-13 to W-19 in `web/src/features/settings/` | Web gate | Pair a browser, see it on the phone's Devices, revoke it from the phone (yes/no) | W0 |
|
||||
| W6 | Android: Insights | Landed `1c9856b` (2026-10-21, D-141 to D-143, SPEC v0.31; Android gate green, 40 `s11_insights_*` goldens). An Insights section on S-11 from the D-133 endpoints, online only (§9.9): a UI-contract change (§14.8) in `:core:model`, `:data`, `:core:fakes` and `:feature:analytics`, with goldens | Android gate | Look at S-11's Insights on the phone (yes/no) | W7 landed, its image deployed |
|
||||
| K0 | Card purchases spike: local test app (D-176) | The test app landed `2ce317f`: `docs/spikes/k0/app.py` and its `README.md`, one command (`python3 docs/spikes/k0/app.py`, the standard library only, the RS256 JWT signed with `openssl`), a page on `http://localhost:8765/` that runs the real Enable Banking flow (bank list, BankID, paste-back of the redirect address, accounts, "Uppdatera" with PSU headers, "Bakgrundshämtning" without, balances, the purchases, an aggregate "Analys", "Koppla från"), key and state in the git-ignored `docs/spikes/k0/.local/`. Still to do: run it with the owner and the partner and write the report `docs/spikes/K0-enable-banking.md` with aggregates only, answering every K0 `TODO(spec)` of §9.12 | The flow works end to end with the owner's card (owner's yes/no); the report answers every K0 item; `git status` shows nothing from `.local/`; no gate (not part of any gate) | Sign up at Enable Banking, accept its Terms of Service, create the restricted production application, move the key, link both cards with BankID, try the test app (PLAN-K0 §8) | SPEC v0.35 |
|
||||
| K1 | Card purchases: server (D-158 to D-175) | Migration 0004 with its contract text and migration test, server schema 4; `src/cards/` (registry, Enable Banking adapter, fetch and de-duplication of §9.12, poller, callback page); E-36 to E-44 in the server and `contracts/openapi.yaml` with examples; new error codes; the link clearing in §14.4.4 step 7; `contracts/test-vectors/match_card_purchase.json` and its Python reference in `tools/contracts-check.py`; config (§10.4), `infra/pi/compose.yaml`, `.env.example`, `infra/dev/**`, `infra/npm/kvitt.conf`, `infra/README.md` (step 4 block, card runbook); `web/src/api/schema.d.ts` regenerated | Server gate; `python3 tools/contracts-check.py`; the web gate (the regenerated types); an integration test suite against a local Enable Banking stub | Copy the key files to `~/kvitt/secrets/cards/`, set the application ids in `.env`, paste the new NPM block, deploy (PLAN-K1 §8) | SPEC v0.36 (K0's answers) |
|
||||
| K2 | Card purchases: Android (D-169, D-170, D-172, D-173) | `:feature:cards` (S-23 to S-27 and the sheet), Room version 2 with its migration test, the `CardRepository` UI contract in `:core:model`, `matchCardPurchase` in `:domain` passing its vectors, `:data` (DTOs, card sync, queued link changes, Custom Tab, deep link), fakes and scenarios, the fourth bottom destination, S-5's "Från kortköp", S-7's prefill and link, S-8's card line, goldens; strings in `docs/ux/strings.csv` | Android gate | Drop the card artwork file; connect the card on the phone with BankID; create an expense from a purchase; link one; disconnect and reconnect (PLAN-K2 §8) | SPEC v0.36; may start then on the spec and fakes; lands after K1 |
|
||||
| K3 | Card purchases: web (D-174) | W-21 to W-23 in `web/src/features/cards/`, the sidebar entry, the S-7-style prefill in W-5 and the card line in W-6, `matchCardPurchase` in TypeScript passing its vectors | Web gate | Drop the web copy of the artwork; connect the card from the laptop browser with BankID QR; create and link from W-21 (PLAN-K3 §8) | SPEC v0.36; lands after K1 and K2 (it takes the card strings K2 adds to `docs/ux/strings.csv`) |
|
||||
| K0 | Card purchases spike: local test app (D-176) | The test app landed `2ce317f`: `docs/spikes/k0/app.py` and its `README.md`, one command (`python3 docs/spikes/k0/app.py`, the standard library only, the RS256 JWT signed with `openssl`), a page on `http://localhost:8765/` that runs the real Enable Banking flow (bank list, BankID, paste-back of the redirect address, accounts, "Uppdatera" with PSU headers, "Bakgrundshämtning" without, balances, the purchases, an aggregate "Analys", "Koppla från"), key and state in the git-ignored `docs/spikes/k0/.local/`. Done 2026-10-24: run with the owner on his card; report `docs/spikes/K0-enable-banking.md` (aggregates only), its answers recorded in SPEC v0.36 (D-177 to D-180); the partner's card and the next-day checks were not run (owner's decision, D-177) and move to K1's owner steps | The flow worked end to end with the owner's card (owner's yes); the report answers every K0 item; `git status` shows nothing from `.local/`; no gate (not part of any gate) | Done 2026-10-24: Enable Banking account and Terms of Service, the restricted production application, the key in `.local/`, the owner's card linked with BankID, the test app run (PLAN-K0 §8) | SPEC v0.35 |
|
||||
| K1 | Card purchases: server (D-158 to D-175) | Migration 0004 with its contract text and migration test, server schema 4; `src/cards/` (registry, Enable Banking adapter, the windowed fetch and de-duplication of §9.12 CP-7 and CP-8, poller, callback page); E-36 to E-44 in the server and `contracts/openapi.yaml` with examples; new error codes; the link clearing in §14.4.4 step 7; `contracts/test-vectors/match_card_purchase.json` and its Python reference in `tools/contracts-check.py`; config (§10.4), `infra/pi/compose.yaml`, `.env.example`, `infra/dev/**`, `infra/npm/kvitt.conf`, `infra/README.md` (step 4 block, card runbook); `web/src/api/schema.d.ts` regenerated | Server gate; `python3 tools/contracts-check.py`; the web gate (the regenerated types); an integration test suite against a local Enable Banking stub | Copy the key (already on the Mac from K0) to `~/kvitt/secrets/cards/`, set the application id in `.env`, paste the new NPM block, deploy; once a client with the card screens runs: the partner's card, the next-day fetch without BankID and pending→booked (D-177) (PLAN-K1 §8) | SPEC v0.36 (K0's answers) |
|
||||
| K2 | Card purchases: Android (D-169, D-170, D-172, D-173) | `:feature:cards` (S-23 to S-27 and the sheet), Room version 2 with its migration test, the `CardRepository` UI contract in `:core:model`, `matchCardPurchase` in `:domain` passing its vectors, `:data` (DTOs, card sync, queued link changes, Custom Tab, deep link), fakes and scenarios, the fourth bottom destination, S-5's "Från kortköp", S-7's prefill and link, S-8's card line, the committed card artwork mapped (D-180), goldens; strings in `docs/ux/strings.csv` | Android gate | Connect the card on the phone with BankID; create an expense from a purchase; link one; disconnect and reconnect (PLAN-K2 §8) | SPEC v0.36; may start then on the spec and fakes; lands after K1 |
|
||||
| K3 | Card purchases: web (D-174) | W-21 to W-23 in `web/src/features/cards/`, the sidebar entry, the S-7-style prefill in W-5 and the card line in W-6, the committed card artwork mapped (D-180), `matchCardPurchase` in TypeScript passing its vectors | Web gate | Connect the card from the laptop browser with BankID QR; create and link from W-21 (PLAN-K3 §8) | SPEC v0.36; lands after K1 and K2 (it takes the card strings K2 adds to `docs/ux/strings.csv`) |
|
||||
| W7 | Server: web serving and analytics endpoints (D-132, D-133) | Landed `50a0d67` (2026-10-21, D-135 to D-138, SPEC v0.29; 299 server tests, contracts-check 136). `/app/` from `KVITT_WEB_DIR` with the SPA fallback, cache headers and the `/app/` CSP; the `/analytics/*` endpoints (D-135 to D-138, SPEC v0.29) in `contracts/openapi.yaml` and the server; the Node stage in `server/Dockerfile` with the repository root as build context (workflow, `infra/build-images.sh`, `infra/dev/**`) | Server gate; `python3 tools/contracts-check.py`; the web gate when `web/` exists and the contract changed (`web/src/api/schema.d.ts` regenerated) | Paste the v0.28 NPM block (`infra/README.md` step 4); deploy the new image (§10.10) | SPEC v0.28 |
|
||||
|
||||
### 11.3 Notes per milestone
|
||||
@@ -1501,7 +1511,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
* **M11** runs the import per §10.8 "Production run" (D-025). The rehearsal on 2026-10-18 ran both real files against a throwaway database and reconciled on both (Vardag 1746 rows, Hemma 37 rows); 21 rows were unmapped before D-044. The dry run of 2026-10-18 with M12b's mapping confirmed it: Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled.
|
||||
* **W0 to W7** (D-134, §15.9): the web client programme. W0 and W7 run first in parallel (disjoint paths); W1 to W5 run in parallel once W0 has landed; W4's W-11 and W6 wait for W7. W0 owns `web/**` except the feature folders, registers every route with a placeholder page per feature folder (as M2a registered every route, D-004), and lists its module paths in its report; W1 to W5 build only inside their feature folders and take everything shared from W0's modules (a missing shared piece is a request to the orchestrator, as M7x's requests went to M6b). A second agent to land that changed `web/package.json` resolves `web/package-lock.json` with `npm install` on rebase (as `Cargo.lock`, §14.7). A plan that changes `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` (§15.3). Web agents do not use the phone or `tools/gradle-serial.sh`; a manual check runs against a throwaway server, never the owner's development or Pi data (§15.4). The orchestrator adds the web gate to `CLAUDE.md` "Build and verify" (D-131). W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, D-150 unused) with the web gate green on `main` (59 test files, 532 tests at `1aecf51`); a web fix (`83f5293`) followed (598 tests). Pieces built twice or three times in feature folders and other loose ends are listed for a W0b plan in `docs/HANDOFF.md` "Web follow-ups".
|
||||
|
||||
* **K0 to K3** (D-176, §11.1): the card-purchase programme. K0 runs first, on the Mac only, with the owner (a local test app for fast feedback before any Android work, owner's refinement 2026-10-24; the app landed `2ce317f`, what is left is running it and the report); its answers to the K0 `TODO(spec)` items of §9.12 go into SPEC v0.36 through a docs agent before K1 starts. Then K1, K2 and K3 run in parallel in their own worktrees (disjoint paths, §14.7): K1 owns the contract (`contracts/**`, the migration text, the vectors) and lands first; K2 and K3 build against the spec and fakes, and rebase on K1 before their gates (K2's DTO tests and K3's generated types need K1's contract); K3 lands after K2, whose `docs/ux/strings.csv` rows it imports (`npm run i18n:import`). K2 uses the phone only when its brief names it; K1 and K3 never use the phone. No plan connects to the owner's real card except through the owner's own BankID step; no plan stores, prints or commits a real purchase.
|
||||
* **K0 to K3** (D-176, §11.1): the card-purchase programme. K0 runs first, on the Mac only, with the owner (a local test app for fast feedback before any Android work, owner's refinement 2026-10-24; the app landed `2ce317f`); K0 is done (run 2026-10-24, report `docs/spikes/K0-enable-banking.md`) and its answers are in SPEC v0.36 (D-177 to D-180), so K1 to K3 can start. The reports of K1 to K3 propose spec text and decisions that a docs agent records in SPEC v0.37. Then K1, K2 and K3 run in parallel in their own worktrees (disjoint paths, §14.7): K1 owns the contract (`contracts/**`, the migration text, the vectors) and lands first; K2 and K3 build against the spec and fakes, and rebase on K1 before their gates (K2's DTO tests and K3's generated types need K1's contract); K3 lands after K2, whose `docs/ux/strings.csv` rows it imports (`npm run i18n:import`). K2 uses the phone only when its brief names it; K1 and K3 never use the phone. No plan connects to the owner's real card except through the owner's own BankID step; no plan stores, prints or commits a real purchase.
|
||||
|
||||
### 11.4 Validation reports
|
||||
|
||||
@@ -3584,3 +3594,4 @@ Plans with disjoint paths (§14.7), one Opus agent each in its own worktree. W0
|
||||
| 0.33 | 2026-10-24 | Owner answers and phone observations of 2026-10-24 (D-156) and the small-edits working rule (D-157). D-156: OQ-6 answered, no confirm step on S-1 for an unusual server (§12); security review L-10 answered, a member naming the other user's device gets `not_found` (AC-3.2, §8.4, E-16; to implement, the server answers `forbidden` today); no host firewall rule limiting port 8080 to `HA_HOST` for now (§10.2, deferred); `mem_limit` kept and advisory on the Pi (§10.2); images keep being built on the Pi 3 runner until the current run's result, `infra/build-images.sh` the fallback, `docker builder prune -af` between builds (runner disk 11 of 15 GB, §10.3); Samsung Auto Blocker must be off for the in-app install (AC-16.3); AC-18.1 verified by the owner, AC-16.3 and the editor keyboard fix observed on the phones, both phones on release 140 (AC-18.1, §11.4); the web client is first used from the laptop browser (§15.2). D-157: small edits by a Haiku or Sonnet agent directly in the main checkout, committed on `main` by the orchestrator (working rule, `CLAUDE.md`). `infra/README.md` "Pi: what the owner does" gains the prune and Auto Blocker notes. contracts-check 136. |
|
||||
| 0.34 | 2026-10-24 | L-10 implemented (`07c36d2`, D-156) and the web shared follow-ups (W0b, `5305f37`), no new decision. L-10: a member naming the other user's device gets E-16's `not_found`, the same body as an unknown id; `contracts/openapi.yaml` drops E-16's 403 and `web/src/api/schema.d.ts` is regenerated (contract change, §14.8); the "to implement" notes of AC-3.2, E-16 (§14.3.4) and §11.4 removed; `Cargo.lock` bumps `ctutils` 0.4.3 → 0.4.4 (RUSTSEC-2026-0385, published 2026-10-23, which failed `cargo audit` on `main`). Server tests 300; contracts-check 136. W0b: shared web pieces once in `components`, `lib`, `api` and `app`, no feature imports another (`catalog.test.ts` also checks `web_` keys); amount fields per page language; Recharts in its own chunk (vendor 516 → 132 kB); Vitest cache off; Testing Library's `findBy`/`waitFor` limit 5 s in `web/src/test/setup.ts` (load on the Mac during parallel agents); the version code from the commit count, else `KVITT_VERSION_CODE`, else 1 (§15.3). Web gate 609 tests. Image build: `server/Dockerfile`'s Node stage takes `ARG KVITT_VERSION_CODE=1` into `npm run build`, and the Gitea workflow passes `--build-arg KVITT_VERSION_CODE=<git rev-list --count HEAD>` (§10.3). |
|
||||
| 0.35 | 2026-10-24 | Card purchases through Enable Banking (owner's request and answers of 2026-10-24, D-158 to D-176) and plans K0 to K3. D-158: card purchases in scope, §2's card and bank non-goal struck through with a pointer, new US-18, US-19, FR-22 (AC-22.1 to AC-22.15), §9.12 (CP-1 to CP-14). D-159: Enable Banking in restricted production for the SJ Prio Mastercard (SEB Kort), G-7 allows it, PSD2 limits stated as facts, ToS as the owner's licence step. D-160: server-only credentials, one adapter `src/cards/enable_banking/`, provider registry, card catalogue E-36. D-161: only the card holder sees their purchases (owner's answer): own endpoints and cursor, not in `GET /sync`, audit, push or analytics, `not_found` for the other user, no card content in logs, session id protected by separation from the private key (§8.1, §8.4, §8.7, §10.9). D-162: connect flow with single-use attempts (E-37, E-38), the callback E-39 and its bilingual page, `kvitt://cards/connected`, polling. D-163: `/cards/callback` through the app gate and never logged (§10.5.2 block). D-164: history 7/30/90/180 days, default 30. D-165: background fetches at 06:45, 11:45, 16:45, 21:45, at most 4 per 24 hours; user-present "Uppdatera" with Enable Banking's PSU headers, once per 60 s; incremental fetch with a 10-day overlap; `card_bank_unavailable`. D-166: consent up to 180 days, notice 14 days before, renewal with BankID; `card_consent_expired`. D-167: migration 0004, server schema 4 (`card_sequence`, `card_connections`, `card_auth_attempts`, `card_purchases`, §14.1.3); disconnect rules (E-43). D-168: transaction mapping, pending→booked de-duplication, foreign currency, credits listed only under "Alla". D-169: "Skapa utgift" prefill, links (E-44), offline-capable link changes, unlink on expense delete (§14.4.4), `expense_already_linked`. D-170: suggested matches (same amount, ±3 days), `matchCardPurchase` with vectors `match_card_purchase.json` (§14.5.8). D-171: no bulk add, no card push (OQ-7, OQ-8). D-172: four bottom destinations, Översikt, Kortköp, Statistik, Inställningar (revises D-048); "Anslut kort" in Settings. D-173: `:feature:cards`, S-23 to S-27, Room version 2, `CardRepository` (§14.2.4), the owner's SJ Prio artwork with a drawn fallback. D-174: web W-21 to W-23 (K3). D-175: `KVITT_EB_*` configuration, `secrets/cards/`, two optional `.env` keys; `cards_unavailable`. D-176: plans K0 (the local test app of `2ce317f`, owner's refinement) to K3; K0's `TODO(spec)` answers go into v0.36 before K1. Endpoints E-36 to E-44; strings for the card screens and the callback page in §13.3; ownership rows K0 to K3 (§14.7); contract files 0004 and the new vector file (§14.8). |
|
||||
| 0.36 | 2026-10-24 | K0's results (the spike run on the owner's SJ Prio Mastercard on 2026-10-24 with `docs/spikes/k0/app.py`, report `docs/spikes/K0-enable-banking.md`), D-177 to D-180; no contract file changes, server schema 4 and Room 2 as specified in v0.35. D-177: K0's outcome; every K0 `TODO(spec)` of §9.12 answered: ASPSP `SJ Prio Mastercard`, `SE` (CP-1); one `CARD` account per session (CP-2); at least 150 days of history right after BankID, so 7/30/90/180 stay (CP-3, §9.6); a background fetch without PSU headers accepted (CP-4); `required_psu_headers` [`psu-ip-address`], compared case-insensitively (CP-5); `maximum_consent_validity` 180 days, `valid_until` 2027-04-22 (CP-6); foreign purchases arrive in SEK (CP-7); credits were card-bill payments (CP-8); and the owner's assumptions of 2026-10-24 for what was not measured: the partner's card links to the owner's application (CP-13, the D-175 fallback stays), a pending transaction keeps its `entry_reference` when booked (CP-8, the amount-and-date rule becomes the secondary rule), the session is reused on later days without BankID (CP-6), all three verified by K1's owner steps. D-178: the card returns at most 100 transactions per response and never a `continuation_key`, so history is fetched in windows of at most 30 days, newest first, a window at 100 rows split in half down to one day, de-duplicated across windows by `provider_ref`; `strategy=longest` dropped; `WRONG_TRANSACTIONS_PERIOD` ends the walk back; only a complete fetch deletes vanished pending purchases (CP-7, CP-8); `card_fetch` counts requests, splits, truncated windows, `booked_by_ref` and `booked_by_match` (§8.7). D-179: transaction mapping from K0's field survey: merchant = `remittance_information`, else `creditor.name`, else "Kortköp"; `purchase_date` = `booking_date`, else `value_date`, else `transaction_date`; `bank_transaction_code` unused, so a debit `FEE` is a purchase to handle; the holder's name, the masked card number (`CPAN`) and the merchant's address are never stored, logged or shown (CP-7). D-180: the owner's SJ Prio Mastercard artwork cropped to the card (1313 × 829) and committed for Android and the web, mapped from the first commit of K2 and K3, the goldens use it, the owner's artwork steps removed (revises D-173, D-174). The K plans' proposed spec text goes into v0.37 (§11.1, §11.3). |
|
||||
|
||||
Reference in New Issue
Block a user