Records D-004 and D-015 to D-025: shells and ownership, server seams (router middleware, rate_limit layer, tasks::start, AuditWriter and PushQueue traits, next_occurrence_date in kvitt-domain, M3 owns recurring generation, M4 lands after M3), OpenAPI parsed-value equality, feature convention and strings, empty fake scenario and resolveRange exception, shared ledger rows and editor fields in :core:design, no bundled font, pruning at 04:00, development infrastructure as M2c-dev built it, M2c-pi allowances, release keystore, production import procedure. Plans and prompts close their open points; HANDOFF rewritten to the real state; CLAUDE.md: implementation agents never edit SPEC.md or docs/decisions.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.9 KiB
PLAN-M9: Splitwise importer
1. Goal
Implement the one-off CLI kvitt-import that imports Splitwise CSV exports, one file per group, into Kvitt accounts, repeatably without duplicates, with audit entries and no pushes. It must read both the English and the Swedish export variants and map Splitwise categories in both languages through one committed table.
2. Read first
- SPEC §10.8 Splitwise import (all of it, including the two header variants and the category table).
- SPEC §4 FR-17 Splitwise import (AC-17.1 to AC-17.6).
- SPEC §12 OQ-2 (answered) and
docs/decisions.mdD-003. - SPEC §6.3 (crate table:
kvitt-importreuseskvitt-serveras a library). - SPEC §14.1 SQLite schema v1 (
import_keycolumns,accounts,expenses,expense_splits,settlements,settlement_allocations,audit_log) and its server-side rules. - SPEC §9.1 Splits (exact rule), §9.6 Limits and validation, §14.4.7 Audit diff (
create), §14.5.2 (compute_split,validate_settlement). - SPEC §11.2 (row M9), §14.7, §14.8.
- Files:
server/crates/kvitt-server/src/lib.rsand the modules it exposes (DB pool, sequence, theAuditWritertrait and M4's implementation, D-015), M2a's shell ofserver/crates/kvitt-import/(D-004),server/Cargo.toml,CLAUDE.md(ANTI-PATTERNS: never commit personal data or the Splitwise CSV).
3. Already established
- Command shape:
kvitt-import --db <path> --file <csv> --account "<Kvitt account name>" --user "<Splitwise name>=<user id>" --user "<Splitwise name>=<user id>" [--fx <CUR>=<SEK per unit>]... [--dry-run](§10.8). - One CSV per group; the account is created with default 50/50 if no non-deleted account with that name exists, else rows go into it (§10.8).
- Two header variants: English
Date,Description,Category,Cost,Currencyand SwedishDatum,Beskrivning,Kategori,Kostnad,Valuta, then one column per member by full Splitwise name; member columns appear in either order, so names are read from the header and matched to--user; never by position; any other header fails with a clear message and writes nothing (§10.8, D-003). - Fields with commas are double-quoted; ISO dates; point decimal; amounts converted to öre exactly by string parsing; a trailing empty line then a
Total balance/Totalsummarow, both ignored, as are blank rows (§10.8). - Expense rows: payer = member with positive net;
share_other = net_payer,share_payer = C − net_payer, modeexact; both nets 0 → payer the owner, modefullon the owner; nets not summing to 0 or a negative share → invalid row, abort, nothing written (§10.8). - Payment rows (
PaymentorBetalning) become settlements:from= positive-net member,to= the other, amount = that net, one allocation to the account,note= description truncated to 200 code points (§10.8). - Non-SEK rows abort unless
--fxis given; thenround_half_up(amount_ore × rate)per value and exact shares recomputed (§10.8). The real exports are all SEK (D-003). import_key= lowercase hex SHA-256 ofsplitwise|v1|<account name>|<Date>|<Description>|<Category>|<Cost>|<Currency>|<member nets in header order, joined with |>|<n>; existing keys are skipped (AC-17.2, §10.8).- Created rows: owner as
created_by_user_idandupdated_by_user_id,updated_by_device_idnull,created_at=updated_at= import time, one auditcreatewithsourceimport, newserver_versions, no pushes (§10.8, AC-17.6, AC-13.3). - Description truncated to 100 code points, full original into
noteswhen truncated (§10.8). - Category mapping: case-insensitive exact match on the category column against the English and Swedish names of the §10.8 table, committed as a data file in the crate; unknown names map to
other, are listed in the summary with row counts, and the total unmapped count is a warning (§10.8, D-003). --dry-runprints rows read, expenses, settlements, skipped duplicates, unknown categories and non-SEK rows, and writes nothing (AC-17.5).- The real CSV files live in
export/in the main checkout (git-ignored); they are never committed, copied into the repository, or quoted in code, fixtures, tests or reports (D-003, CLAUDE.md).
Mapping notes (recorded here and in D-003 only): the English export (group "hemma", account "Hemma") has the header line Date,Description,Category,Cost,Currency,Amelie Bennich,Jacob Hagstedt; the Swedish export (general group, account "Vardag") has Datum,Beskrivning,Kategori,Kostnad,Valuta,Jacob Hagstedt,Amelie Bennich. The owner is Jacob Hagstedt; the partner is Amelie Bennich, display name Amelie. Fixtures use synthetic member names, never these.
4. Owned paths
From SPEC §14.7, verbatim: server/crates/kvitt-import/** (owner M9; after landing: "same"). Also from §14.7: M9 "may add a dependency line to a crate's Cargo.toml (second to land resolves Cargo.lock on rebase)".
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
5. Deliverables
server/crates/kvitt-import/Cargo.toml: depends onkvitt-domainandkvitt-server(library).server/crates/kvitt-import/src/main.rs: CLI parsing per §10.8.server/crates/kvitt-import/src/**: CSV reader (quoting, both header variants, member matching by name), row classification (expense, payment, total, blank), amount parsing, split derivation, fx conversion, import key, category mapping, writer (one transaction, audit through theAuditWritertrait, nothing throughPushQueue), summary.server/crates/kvitt-import/data/splitwise_categories.csv: the mapping table, columnssplitwise_name,language,default_key, every English and Swedish name of the §10.8 table (location fixed in §10.8 and §14.7, v0.3).server/crates/kvitt-import/tests/fixtures/*.csv: synthetic CSVs only (English variant, Swedish variant with reversed member order, quoted commas, payments in both languages, totals rows in both languages, blank rows, a non-SEK row, a truncated description, an unknown category, an invalid row, duplicate identical lines).server/crates/kvitt-import/tests/*.rs: integration tests against a temp database created with the server's migrations.
6. Steps
- Worktree. From the main checkout
/Users/jhag/projects/kvitt:git worktree add .claude/worktrees/agent-m9 -b agent-m9 main, then in.claude/worktrees/agent-m9:git reset --hard main. Do not copyexport/into the worktree. Runtools/install-git-hooks.sh. - Read §2 in order. Confirm M2a and M4 have landed (the audit writer exists); if not, stop and report.
- Versions: a CSV crate (for example
csv) and a CLI parser at latest stable from crates.io / docs.rs; record each. - Write the category table file from §10.8 and a test that every name of the §10.8 table appears in it exactly once.
- Write the synthetic fixtures, then the tests (AC-17.1 to AC-17.6, both variants, member order, header failure, invalid row aborts with nothing written, re-run creates nothing, dry-run writes nothing, fx conversion, no pushes, audit
sourceimport). - Implement until the tests pass.
- Optional real-file dry run, only if the start prompt allows: run
kvitt-import --dry-runagainst a throwaway database with each file in/Users/jhag/projects/kvitt/export/, report aggregates only (row counts, unmapped category names with counts; no descriptions, no amounts). - Run the gate (§7). Commit on
agent-m9,git rebase main, rerun the gate. Never merge, push or remove the worktree. Checkgit statusshows no CSV fromexport/.
7. Gate
From SPEC §11.2, verbatim: "Server gate".
Command, from server/: cargo fmt --check && cargo clippy -- -D warnings && cargo test.
Green means: all three exit 0 in this session; test result: lines for kvitt-import show 0 failed and include each case of step 5.
8. HUMAN steps
From SPEC §11.2, verbatim: "Provide the CSV files (kept outside git) when OQ-2 is answered".
Where in the flow: already done; both exports are in export/ (git-ignored) and OQ-2 is answered (D-003). They are only used by the optional dry run of step 7; the gate uses synthetic fixtures.
9. Out of scope
- Running the real import into the production database (M11).
- Changing the server schema, audit writer or sync (contract request).
- Any export feature (§2 Non-goals); any Android work.
- Committing, copying or quoting the real CSV files.
10. Report
- Branch
agent-m9, head commit hash after rebasing onmain. - Gate output: fmt and clippy exit status, every
test result:line. - Versions of crates added, with sources.
- Optional dry-run aggregates (row counts per file, unmapped category names with counts), if run.
- Proposed decision entries (never written to
SPEC.mdordocs/decisions.mdby this agent) orTODO(spec)occurrences. - Contract requests, open questions, spec gaps (with section numbers).
11. Numbering
Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use TODO(spec) if none was given.