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>
8.8 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, 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,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 the server's audit writer), 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 chosen inside the crate because §14.7 givescontracts/**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
- 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.
- 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.