Skip to content

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.