1 changed files with 212 additions and 0 deletions
@ -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. |
||||
Loading…
Reference in new issue