- D-055 build-time copy of the keyword table, D-056 parseCategoryKeywords, D-057 enum declaration orders, D-058 E-7 example shows server_version 15 and 16 and its test compares them again. - D-051 fulfilled: §9.7 hex values from docs/ux/design-spec.md §2.3; design-spec.md binding, strings.csv the source of string resources. - D-059: ExpenseEditorOptions.frequentCategoryIds and CategoryRepository.deletable; new milestone M12d with PLAN-M12d and prompts/M12d.md; M7a-f depend on M6 and M12d. - suggestCategory readings (null when other is archived), §11.2 landed marks and the 2026-10-18 dry run, dev stack on schema 2, PLAN-M12b SQLite 3.51.x, design-spec §6 notes in PLAN-M6/M7c/M7f; HANDOFF rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
12 KiB
PLAN-M7c: Feature expense
1. Goal
Build :feature:expense: the full-screen expense editor (S-7) and the expense detail with its timeline (S-8), against the :core:model interfaces and :core:fakes. Every state of both screens is screenshot-tested in Swedish and English, light and dark.
2. Read first
docs/ux/design-spec.md: tokens, component inventory, and the sections for S-7, S-8.docs/ux/strings.csv(keys for these screens) anddocs/ux/screens/S-7-*,docs/ux/screens/S-8-*.- SPEC §13.2 Navigation model and §13.3 Screens (rows S-7, S-8 and the states paragraph); §9.11 Category suggestion (D-049). M12c must have landed (§11.2). After saving, the snackbar "Utgiften sparades" has no undo (S-7, D-052).
- SPEC §14.2.4 UI contract:
ExpenseRepository,ExpenseDraft,ExpenseEditorOptions,ExpenseDetailState,TimelineLine,FieldChange,Share,SaveResult,FieldError,SplitMode,Account,Category,User. - SPEC §4 FR-8 Expenses (AC-8.1 to AC-8.11); FR-12 AC-12.4, AC-12.5; §9.1 Splits; §9.6 (limits, amount input parsing); §9.10 (exact-mode edit); §14.5.3 error tokens.
- SPEC §6.2 (module table), §9.8 Formatting, §9.9 Online-only actions, §11.2 (row M7c), §11.3 (note M7x), §14.7.
- Files:
android/core/model/**,android/core/design/**(read only),android/core/fakes/**,android/build-logic/**,CLAUDE.md(BUILD AND VERIFY: Screenshots; ARCHITECTURE RULES; ANTI-PATTERNS).
3. Already established
- Features depend only on
:core:model,:core:designand:core:fakes;:core:fakesonly in test, screenshot-test and debug-preview configurations, never in a release build. Features never depend on:dataor:domain(§6.2, §11.3, CLAUDE.md Layering). - Screens consume the
*Stateclasses through the repository interfaces of §14.2.4; no field may be added except through a contract change (§14.2.4, §14.8). - Fake scenarios go only in
android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/<feature>/(§11.3, §14.7). - Strings live in the module's
src/main/res/values/strings.xmlandvalues-sv/strings.xmlwith keys fromdocs/ux/strings.csv; no string outside that file unless the report proposes it as a decision entry for the orchestrator (§13.4, §14.7, AC-19.1). - No hard-coded colours, sizes or strings; tokens and shared components come from
:core:design, which features may not edit (CLAUDE.md, §14.7). Money and dates go throughMoneyFormatterandDateFormatter(§9.8). - Online-only actions are disabled with "Needs connection" when offline (§9.9); a REST failure shows "Could not reach the server".
- Feature convention (§6.2, D-017): one composable entry point per screen, taking a
hiltViewModel()-provided ViewModel and its navigation as plain lambdas; the feature never references another feature or theNavHost; ViewModels get repositories through the:core:modelinterfaces. Navigation graph, deep links and Hilt bindings are in:app(M8). - M2b created this module's shell with one empty entry composable (D-004); this plan owns it and replaces it with the real entry points.
- Keys are prefixed by the feature name; lint
MissingTranslationis an error in this module (§6.2, §13.4, D-017). - The states each screen must have are the ones
docs/ux/design-spec.mdlists for it (§13.3). - Shared UI (D-019): the expense editor fields (S-7) come from
:core:design, built by M6; never copy or re-implement it here. - Layout and visuals follow
docs/ux/design-spec.md; TalkBack labels as it specifies; 48 dp targets, 200 % font scale reflow (§13.1). - No placeholder text in previews or screenshots; goldens are committed; open every changed PNG before committing (CLAUDE.md).
- The add flow needs at most: open app, tap add, type amount, type description, tap save; account, category, payer and split are defaulted (§13.1, AC-8.6).
- Editor is a full screen, not a bottom sheet (§13.2). Amount input accepts digits, one decimal separator (
,or.), at most two decimals, no sign, converted to öre by string parsing, never floating point (§9.6). - Defaults: account = last used on this phone if active else oldest active; category for a new expense = the suggestion of §9.11 (D-049): the editor keeps a "picked by hand" flag (CS-2) and, while it is false, calls
ExpenseRepository.suggestCategory(accountId, description)on every description and account change (CS-3), showing the result with no marker (CS-7); before any description it isExpenseEditorOptions.defaultCategoryFor(last used in that account if active elseother); editing never changes the category (CS-1); split modedefault; payer = current user; date = today in the phone's zone (AC-8.3, AC-8.4, AC-8.6). Defaults come fromExpenseEditorOptions. - Split modes
default("Account default 60/40"),ratio,exact(type one share, the other is amount minus it, read-only; a share larger than the amount blocks saving inline),full(one bearer); changing account indefaultre-copies the ratio; live preview fromExpenseRepository.preview(AC-8.5, AC-8.7, AC-8.8, §13.3 S-7). - Validation errors come back as
SaveResult.Invalidwith §14.5.3 tokens,required, orvalidation_failedfor a §9.6 limit without a domain token (lengths, dates) (§9.6, §14.2.4); deleting asks for confirmation (AC-8.9). - Category quick row (D-059, SPEC §14.2.4 "Changes in v0.9"):
ExpenseEditorOptions.frequentCategoryIds[accountId], read once on open and on an account change, filled up to five from the remaining active categories insort_order; the selected category (suggested, defaulted or picked in "Alla") takes the fifth place when it is not among them. M12d must have landed (§11.2). When the suggestion returns null (otherarchived and no active last-used category), the editor shows no category until the user picks one, and saving is blocked as for a missing category (AC-8.6). - Design-spec open points (
docs/ux/design-spec.md§6) carried here: tabular figures for amounts as M6 delivers them (M6 checks them on the S26); Material 3 Expressive components only as M6 provides them, with M6's stable fallbacks; the recurring editor S-19 (M7f) has no category suggestion, so nothing in this feature is shared with it beyond the:core:designeditor fields. - Detail shows amount, description, category, account, date, payer, shares and mode, notes, "Recurring: " when generated, and the timeline: newest last, one line per entry, field names in the UI language, ids resolved, pending line "Your change is waiting to sync"; a deleted state when opened from a deletion notification (AC-8.11, AC-12.4, AC-12.5, §13.3 S-8).
4. Owned paths
From SPEC §14.7, verbatim:
android/feature/expense/**(owner M7c), and from theandroid/core/fakes/**row: "M7x add files only underfakes/<feature>/", hereandroid/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/expense/(§11.3). Catalog entries and module includes may be added (§14.7,android/gradle/**row).Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
5. Deliverables
android/feature/expense/build.gradle.kts: dependencies:core:model,:core:design;:core:fakesfor tests and debug only.android/feature/expense/src/main/kotlin/se/hagstedt/kvitt/feature/expense/**: the screens of this module and their state holders.android/feature/expense/src/main/res/values/strings.xml,values-sv/strings.xml: keys fromdocs/ux/strings.csv.android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/expense/**: a scenario per state below.- Screenshot tests and committed goldens for every screen × state × light/dark × sv/en.
- Unit tests for the state holders (input handling, enablement rules, error mapping).
Screens and states. Required states for every screen below (SPEC §13.3): loaded, empty, offline (with pending changes), loading (first sync only), error (a failed online action), and for lists long content (long Swedish names, 7-digit amounts), each where it applies. A state that
docs/ux/design-spec.mdmarks not applicable for a screen is skipped and named in the report. Each state is screenshot-tested in light and dark, Swedish and English. Screen-specific states from §13.3 "Must show" are added per screen:- S-7: new (defaults applied); edit mode with Delete; each split mode
default,ratio,exact,full(on payer and on the other user); exact share larger than the amount (inline error, save blocked); missing description and missing amount errors; notes collapsed and expanded; account picker open; category grid open; date picker; delete confirmation; offline (saving still allowed); long content (long description, 7-digit amount, long account and category names). - S-8: loaded in each split mode; recurring; with notes; timeline with create, change (amount, account, split) and pending lines; deleted; pending; offline; long content (long timeline).
6. Steps
- Worktree. From the main checkout
/Users/jhag/projects/kvitt:git worktree add .claude/worktrees/agent-m7c -b agent-m7c main, then in.claude/worktrees/agent-m7c:git reset --hard main. Copyandroid/local.propertiesfrom the main checkout (addsdk.dir=/opt/homebrew/share/android-commandlinetoolsif missing; never overwrite or commit). Runtools/install-git-hooks.sh. - Read §2 in order. Confirm M6 has landed (
android/core/design/has components onmain); if not, stop and report. If a component the design spec assigns to:core:designis missing, file a contract request; do not build it inside the feature. - Run
python3 tools/dependency-report/latest.py; record the lines. No new library is expected; any one added is at its latest stable version from official docs; record it. - Write the fake scenarios under the feature's
fakes/<feature>/directory for every state listed in §5. - Write the state holders and screens, one screen at a time, with strings from
docs/ux/strings.csvin both languages. - Write the screenshot tests; record goldens; open every PNG and compare with
docs/ux/screens/. - Run the gate (§7). Commit on
agent-m7c,git rebase main, rerun the gate. Never merge, push or remove the worktree.
7. Gate
From SPEC §11.2, verbatim: "as M7a", that is "Android gate with screenshot tests of every state, sv and en, light and dark". Command, from
android/:PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check. Green means:BUILD SUCCESSFULin this session; screenshot verification passes for:feature:expense; the report lists, per screen, every state × light/dark × sv/en golden; unit tests 0 failures in fresh XML.8. HUMAN steps
From SPEC §11.2, verbatim: "none".
9. Out of scope
- Other screens: S-1, S-2, S-3, S-18 (M7a); S-4, S-5, S-6, S-16, S-17 (M7b); S-9, S-10 (M7d); S-11, S-12 (M7e); S-13, S-14, S-15, S-19, S-20, S-21, S-22 (M7f).
- Navigation graph, deep links, Hilt bindings, notifications (M8); real repositories (M5).
- Editing
:core:design(requests go to the orchestrator) or:core:model; any dependency on:dataor:domain. - Fake scenarios outside this plan's
fakes/<feature>/directories.
10. Report
- Branch
agent-m7c, head commit hash after rebasing onmain. - Gate output: BUILD line, actionable tasks line, screenshot task result, test counts from fresh XML.
- Screens × states table with golden file names; states skipped as not applicable.
- Versions added (if any) with sources; dependency-report lines.
- Strings used that are missing from
strings.csv(should be none) and design-spec mismatches. - Proposed decision entries (never written to
SPEC.mdordocs/decisions.mdby this agent) orTODO(spec)occurrences. - Contract requests (
:core:design,:core:model), 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.