Statements Page¶
Route: /statements (statements.html)
Nav label: Statements
Last Updated: 2026-08-11
What It Does¶
Three functions in one page: audit which accounts are behind on statements, upload statement files for parsing, and record a balance by hand for accounts with no parser.
Replaces the older Record Balances page, which still renders at
/record-balances but is no longer linked from the nav.
Sections¶
Statement Audit¶
One row per active account: name, nickname, institution, type, display_group, expected
statement_frequency, the date of the newest recorded balance, and how many balances exist.
From GET /api/statements/audit, which is a LEFT JOIN from accounts to account_balances. An
account with no balances at all still appears, with a null date — that is the row worth acting on.
Nicknames are editable inline via PATCH /api/accounts/<id>/nickname.
Upload Statement¶
Multi-file upload to POST /api/statements/upload, field name files. Parsing is synchronous and
in-process — the response carries the results, no cron involved.
Upload → POST /api/statements/upload (field: files)
→ detect_bank() identifies the institution
→ parse() extracts transactions, closing date, balances
→ insert_bank_statement() writes bank_statements rows (all of them first)
→ insert_transactions() writes bank_transactions rows
→ upsert_account_balance() updates account_balances
→ response: {ok, balances_saved, errors[]}
statement_files_processed deduplicates by file hash, so re-uploading the same PDF is a no-op rather
than a duplicate.
The alternative path is the separate statements.heezy.info service,
which only drops files onto the NFS ingest volume; heezy-statement-scanner picks them up at :30
past the hour. Same parser, different trigger.
Record a Balance Manually¶
For Edward Jones-style accounts where a balance is known but no parser handles the document. Pick an account, enter a balance and an as-of date, submit.
Manual balance entry is broken
The form POSTs to /api/account-balances with account_id in the JSON body. No such route
exists. app.py only defines /api/account-balances/<int:account_id>, so the request returns
404 and the UI reports a failure. The legacy record-balances.html has the identical bug.
The working call is:
curl -X POST http://192.168.1.15:30860/api/account-balances/<account_id> \
-H 'Content-Type: application/json' \
-d '{"balance": 1234.56, "as_of_date": "2026-08-11"}'
Note the endpoint ignores a source field and always writes source = 'manual'. Fixing the
templates to append the id to the URL is the smaller change; adding a body-based route is the
other option.
The insert is ON CONFLICT (account_id, as_of_date) DO UPDATE, so re-recording the same day
overwrites rather than duplicating.
API Endpoints Used¶
| Endpoint | Purpose |
|---|---|
GET /api/statements/audit |
Per-account last statement date, frequency, and count |
GET /api/accounts |
Account list for the balance form |
POST /api/statements/upload |
Upload and parse statement files (field: files) |
PATCH /api/accounts/<id>/nickname |
Rename an account for display |
POST /api/account-balances/<id> |
Record a balance. The page calls this without the id and 404s |
Supported Formats¶
Bank of America checking and credit cards, Capital One checking and savings (including multi-account PDFs), Chase Amazon Visa (PDF, CSV, OFX/QFX), Edward Jones (brokerage, 529, IRA, Roth IRA, money market), Fifth Third mortgage. BoA year-end summary PDFs are recognized and skipped rather than mis-parsed.
See Data Sources for the detection order and per-bank quirks.
Troubleshooting¶
kubectl logs -n heezy -l app=heezy-finance --tail=50
kubectl logs -n heezy -l job-name=heezy-statement-scanner --tail=50
Account must exist before upload
If no accounts row matches on institution + last4 + type, upsert_account_balance() logs a
WARN and silently skips the balance. The upload still reports success. Insert the account row
first — required columns are in Data Sources.