Files
kvitt/PLAN-M9.md
T
jacobh2 76ee957f41 SPEC v0.3: orchestrator resolutions of the plans' open points
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>
2026-10-17 21:23:38 +02:00

8.9 KiB
Raw Permalink 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, the AuditWriter trait and M4's implementation, D-015), M2a's shell of server/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,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 AuditWriter trait, nothing through PushQueue), 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 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

  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.
  • Optional dry-run aggregates (row counts per file, unmapped category names with counts), if run.
  • Proposed decision entries (never written to SPEC.md or docs/decisions.md by this agent) 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.