Files
kvitt/PLAN-M9.md
T
jacobh2 5e1be38364 Plans and start prompts for every milestone; SPEC v0.2
PLAN-<id>.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6,
M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a
start prompt per plan in prompts/ plus prompts/README.md (dependency
order and parallel waves).

SPEC v0.2: owner answers D-001 (development-first, M2c split into
M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once
on Drive), D-003 (Splitwise CSV format, English and Swedish variants,
category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of
ambiguous contract points stated explicitly in §14.3 to §14.5.
HANDOFF §2 lists the plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:05:34 +02:00

8.8 KiB
Raw Blame History

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

  1. SPEC §10.8 Splitwise import (all of it, including the two header variants and the category table).
  2. SPEC §4 FR-17 Splitwise import (AC-17.1 to AC-17.6).
  3. SPEC §12 OQ-2 (answered) and docs/decisions.md D-003.
  4. SPEC §6.3 (crate table: kvitt-import reuses kvitt-server as a library).
  5. SPEC §14.1 SQLite schema v1 (import_key columns, accounts, expenses, expense_splits, settlements, settlement_allocations, audit_log) and its server-side rules.
  6. SPEC §9.1 Splits (exact rule), §9.6 Limits and validation, §14.4.7 Audit diff (create), §14.5.2 (compute_split, validate_settlement).
  7. SPEC §11.2 (row M9), §14.7, §14.8.
  8. Files: server/crates/kvitt-server/src/lib.rs and the modules it exposes (DB pool, sequence, audit writer), 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,Currency and Swedish Datum,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 / Totalsumma row, both ignored, as are blank rows (§10.8).
  • Expense rows: payer = member with positive net; share_other = net_payer, share_payer = C − net_payer, mode exact; both nets 0 → payer the owner, mode full on the owner; nets not summing to 0 or a negative share → invalid row, abort, nothing written (§10.8).
  • Payment rows (Payment or Betalning) 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 --fx is given; then round_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 of splitwise|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_id and updated_by_user_id, updated_by_device_id null, created_at = updated_at = import time, one audit create with source import, new server_versions, no pushes (§10.8, AC-17.6, AC-13.3).
  • Description truncated to 100 code points, full original into notes when 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-run prints 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 on kvitt-domain and kvitt-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 the server's audit writer), summary.
  • server/crates/kvitt-import/data/splitwise_categories.csv: the mapping table, columns splitwise_name,language,default_key, every English and Swedish name of the §10.8 table (location chosen inside the crate because §14.7 gives contracts/** to M1; the orchestrator may move it).
  • 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

  1. 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 copy export/ into the worktree. Run tools/install-git-hooks.sh.
  2. Read §2 in order. Confirm M2a and M4 have landed (the audit writer exists); if not, stop and report.
  3. Versions: a CSV crate (for example csv) and a CLI parser at latest stable from crates.io / docs.rs; record each.
  4. Write the category table file from §10.8 and a test that every name of the §10.8 table appears in it exactly once.
  5. 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 source import).
  6. Implement until the tests pass.
  7. Optional real-file dry run, only if the start prompt allows: run kvitt-import --dry-run against 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).
  8. Run the gate (§7). Commit on agent-m9, git rebase main, rerun the gate. Never merge, push or remove the worktree. Check git status shows no CSV from export/.

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 on main.
  • Gate output: fmt and clippy exit status, every test result: line.
  • Versions of crates added, with sources.
  • Location of the category table file.
  • Optional dry-run aggregates (row counts per file, unmapped category names with counts), if run.
  • Numbering used or TODO(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.