diff --git a/docs/superpowers/specs/2026-09-07-local-mvp-workflows-design.md b/docs/superpowers/specs/2026-09-07-local-mvp-workflows-design.md new file mode 100644 index 0000000..8b8becf --- /dev/null +++ b/docs/superpowers/specs/2026-09-07-local-mvp-workflows-design.md @@ -0,0 +1,212 @@ +# Local MVP Workflows Design + +## Purpose and scope + +Roadmap step 5 gives the local owner a server-rendered Django interface for +managing reference data and transactions, categorizing imported activity, and +inspecting monthly summaries. It also provides durable, local exports and +PostgreSQL backup and restore-verification commands. Together these complete a +documented synthetic import-to-report-to-backup workflow. + +The reviewed-import parser and decision workflow remains command based. The web +application begins with the committed financial records and links to the local +import instructions. Bank APIs, investments, forecasting, receipt recognition, +mobile clients, hosted multi-user operation, and automatic or AI categorization +remain outside the MVP. + +## Chosen approach + +Use Django templates, forms, and ordinary HTTP requests. Keep read behavior in +explicit query and report modules, presentation in `core`, and all mutations in +the existing audited `finance.services` boundary. This adds no JavaScript +framework and does not expose Django admin as a financial editor. + +Alternatives considered were a command-only workflow and a client-rendered API +application. Command-only operation would not provide the planned local screens. +A separate API and JavaScript client would add authentication, serialization, +state-management, and build-tooling work without improving this single-owner +milestone. + +## Application surface + +The root page is a monthly dashboard. The local navigation exposes accounts, +transactions, categories, payees, reports, and exports, plus a link to the +reviewed-import guide. Server-rendered pages use semantic HTML and remain usable +without client-side JavaScript. + +Transaction pages provide: + +- a paginated list ordered by posting date and stable identifier; +- filters for inclusive date range, account, kind, payee, category, currency, + and uncategorized status; +- a detail page with relationships and source provenance; +- manual creation through the supported financial posting service; and +- annotation of description, payee, and category through + `annotate_transaction`. + +The interface does not delete transactions or rewrite amount, posting date, +occurrence date, account, kind, refund, transfer, statement, installment, or +source identity. Financial corrections are represented by new entries, refunds, +or transfers through the domain services. Imported transaction evidence remains +immutable. + +Reference pages list active and archived accounts, categories, and payees. +Creation and archival use `create_account`, `create_category`, `create_payee`, +and `archive`. Archived references remain visible in historical records and +cannot be selected for new assignments. + +## Request security and accountability + +All mutation forms use Django CSRF protection and POST requests. Views never +call model `save()` directly for financial writes. The local server derives the +actor as `Actor(kind="HUMAN", identifier="local-owner")`; actor identity is not +accepted from request data. Domain errors become safe form messages keyed by +their stable error codes and never include transaction descriptions or source +rows. + +The application continues to bind only to loopback. Formal login and multi-user +authorization are outside this local single-owner release. Django session and +message support may be enabled only to carry local form feedback; no financial +values are stored in cookies. + +## Query boundaries + +`finance.queries` owns reusable read models for transaction and reference lists. +It validates filter values, applies deterministic ordering, selects related +objects deliberately, and returns querysets or immutable result values without +mutating records. Views remain responsible for HTTP parsing and pagination. + +`finance.reports` owns aggregate calculations and returns immutable report +values. It performs decimal aggregation in PostgreSQL and never combines +currencies. Empty periods return explicit zero totals and empty category rows. + +Only query shapes used by the screens and exports receive indexes. Initial +indexes cover transaction posting date and the common account-plus-posting-date +path. Their migrations are verified against PostgreSQL. + +## Reporting semantics + +All monthly reports use `posting_date` and an inclusive calendar month selected +by the owner. Every report selects exactly one currency. Archived accounts and +references remain included when they have activity in the selected period. + +The monthly cash-flow summary uses economic activity across bank and credit-card +accounts: + +- `INCOME` contributes its positive amount to inflow; +- `EXPENSE` contributes the absolute value of its negative amount to spending; +- `REFUND` reduces spending in the month in which the refund posts; +- `TRANSFER` is excluded, including credit-card payments; and +- `OPENING` is excluded. + +Net activity is inflow minus net spending. Net spending may be negative when a +month contains refunds greater than expenses. A card purchase is counted as +spending when it posts; its later card-payment transfer is excluded, so it is +not counted twice. + +The spending summary groups `EXPENSE` and `REFUND` amounts by expense category. +An explicit Uncategorized row includes transactions without a category. +Refunds use their own assigned category and are not automatically inherited from +the original purchase. Rows show expense, refund, and net-spending totals and +sort by net spending descending with a stable category-name tie-breaker. + +Account balances are separate from cash flow. A balance is the signed sum of all +entries through the selected month end, including openings and transfer legs. +Balances are reported per account and currency and are never summed across +currencies. + +## Export format and safety + +An export service and management command create a versioned ZIP package under +the ignored repository-root `exports/` directory. The browser can request the +same package and receives it as a download; it does not choose a server +filesystem path. + +Each package contains a JSON manifest and UTF-8 CSV files for accounts, payees, +categories, statements, installment plans, transactions, transfers, and source +records. Stable UUIDs, ISO-8601 dates and timestamps, exact decimal strings, +archival state, relationship UUIDs, and provenance metadata are retained. +Audit events and import-review working state belong to backups rather than the +portable financial export. + +The manifest records the export schema version, creation time, application +version, file names, row counts, and SHA-256 checksums. CSV headers and file +ordering are fixed by export schema version. Database reads occur inside one +consistent transaction so the package represents one snapshot. + +Command output contains only the package name, version, and row counts. Export +names are generated or validated as plain file names. Paths, traversal, +symlinks, existing destinations, and files outside `EXPORT_ROOT` are rejected. +The package is written to a private temporary file, flushed, and atomically +renamed. Failures remove the temporary file without replacing an existing +export. + +## PostgreSQL backups and restore verification + +Backup management commands invoke the PostgreSQL 17 `pg_dump` and `pg_restore` +clients from the application image. They use the management service's migration +database credentials and never accept passwords as command arguments. Backups +use PostgreSQL custom format with `--no-owner` and `--no-acl` and are stored +under the ignored repository-root `backups/` directory. + +Each backup has a JSON manifest containing format version, creation time, +application version, PostgreSQL client version, database name, applied Django +migrations, byte size, and SHA-256 checksum. Creation uses a private temporary +file, `fsync`, and atomic rename; existing targets are never overwritten. +Terminal output excludes financial data and credentials. + +Restore verification requires a database connection explicitly marked as a +verification target and rejects the configured live database host/name pair. +The target database must be empty. Verification checks the manifest and digest, +restores with `pg_restore --exit-on-error --single-transaction`, then runs Django +schema checks and deterministic row-count/integrity checks. The acceptance test +uses a disposable PostgreSQL service populated only with synthetic data. + +Restoring over the live database is not part of the web UI or automated +verification. The owner-facing recovery guide documents a deliberate command +sequence and requires stopping the web service, creating a separate safety +backup, and naming the target database explicitly. + +## Configuration and storage + +`EXPORT_ROOT` and `BACKUP_ROOT` default to `/app/exports` and `/app/backups` in +containers. Docker Compose mounts the ignored repository directories into the +management and web services with only the access each workflow needs. The +PostgreSQL client major version matches the PostgreSQL 17 server. + +Generated exports, backups, temporary files, database dumps, reports, and logs +remain ignored. Automated tests use synthetic records and temporary directories. +No fixture or assertion contains copied financial data. + +## Error handling + +Form validation errors stay on the submitted page without mutation. Domain +conflicts are mapped to stable, human-readable local messages. Missing objects +return 404. Invalid report and list filters return field-level errors rather +than silently changing the requested range. + +Export and backup commands fail closed for invalid paths, existing files, +checksum mismatch, incompatible format versions, unavailable PostgreSQL tools, +database errors, or nonempty verification targets. They emit stable safe error +codes and leave no published partial artifact. + +## Verification and completion + +Unit and PostgreSQL-backed tests cover query filters, pagination stability, +report calculations, currency separation, refunds, transfers, openings, +uncategorized spending, archived references, and account balances. Request tests +cover page rendering, CSRF-protected POST workflows, audited service writes, +immutable financial fields, and safe errors. + +Export tests inspect package schemas, ordering, exact decimals, manifests, +checksums, snapshot behavior, path rejection, non-overwrite behavior, and failed +write cleanup. Backup integration tests create a synthetic database state, +produce a dump, restore it into a separate empty database, verify migrations and +row integrity, and confirm the original database is unchanged. + +The documented acceptance workflow creates synthetic reference data, previews +and commits a synthetic import, categorizes a transaction in the browser, +inspects monthly cash flow and spending, downloads an export, creates a backup, +and verifies restoration into the disposable database. Roadmap step 5 is +complete when that workflow and the full automated suite pass from the Docker +Compose environment.