heezy-finance Data Standards¶
Canonical reference for date fields, numeric types, category values, identifiers, and source tracking across the heezy finance database. Read this before adding a table or a parser.
Last audited: 2026-08-11
Date & Timestamp Rules¶
| Rule | Detail |
|---|---|
Never store dates as text in new tables |
Use date (no time needed) or timestamptz (if time matters) |
| All timestamps stored in UTC | Display converts to America/New_York via NY_TZ in app.py |
date type for statement and transaction dates |
e.g. bank_transactions.date DATE |
timestamptz type for ingest and processing times |
e.g. inserted_at TIMESTAMPTZ DEFAULT now() |
Current State¶
| Table | Column | Type | Status |
|---|---|---|---|
bank_transactions |
date |
date |
Clean |
bank_statements |
period_end_date |
date |
Clean |
bank_statements |
statement_period_start |
text |
Tech debt. Raw text from the PDF, not normalized |
bank_statements |
statement_period_end |
text |
Tech debt. Raw text from the PDF, not normalized |
orders |
order_date |
text |
RFC 2822 raw text from the email |
orders |
order_date_ts |
timestamptz |
Normalized form. Use this. 0 NULL rows |
receipts |
date |
text |
Historical text, multiple formats |
receipts |
date_ts |
timestamptz |
Normalized form. Use this. 12 of 155 rows are NULL |
receipts |
upload_date |
text |
Upload timestamp stored as text |
account_balances |
as_of_date |
date |
Clean |
mortgage_statements |
statement_date, next_payment_due |
date |
Clean |
statement_files_processed |
as_of_date |
date |
Clean |
* |
inserted_at |
timestamptz |
Present on every ingest table |
receipts.date_ts is no longer fully backfilled
The June 2026 backfill covered all 51 rows that existed then. Receipt volume has since tripled to
155 and 12 rows have a NULL date_ts. Anything filtering on date_ts silently drops them.
Backfill from the text column where it parses:
UPDATE receipts SET date_ts = date::date::timestamptz WHERE date_ts IS NULL AND date ~ '^\d{4}-\d{2}-\d{2}'
Always use the _ts column
In app.py, filter and display on order_date_ts and date_ts, never the raw text columns. The
text columns exist for audit trail only.
Rule for New Parsers¶
Every new parser must:
- Parse the date into a Python
datetime - Store it as
date(statement and transaction dates) ortimestamptz(timestamps) - Never write a raw date string into a
textcolumn
Numeric / Amount Rules¶
Rule: numeric(12,2) for all money columns. Never real or float.
The real tech debt is resolved. Every money column across receipts, receipt_items, orders,
order_items, bank_statements, bank_transactions, account_balances, mortgage_statements,
spend_budgets, and annual_budgets is now numeric. receipt_items.quantity is numeric,
order_items.quantity is integer.
Nothing further is planned here. The old "Phase 3 migration target" list is retired.
Category Standards¶
Categories are the single source of truth for spending classification across bank_transactions,
receipt_items, and order_items.
The canonical list lives in categories.py, not app.py. app.py re-exports CATEGORY_ICONS,
CATEGORIES, ANNUAL_ONLY_CATS, and FIXED_COST_CATS from it for templates and other modules;
item_classifier.py imports ITEM_CATEGORIES and normalize_category from the same file. Keeping
one list in one module is deliberate: the classifier's private copy drifted from app.py's for three
months.
See Categories for the full 27-entry table, the four subsets, and the alias map.
No FK enforcement
There is no categories lookup table and no FK constraint. Category values are free text.
Validation happens in Python: normalize_category() on ingest, and a membership check against
CATEGORIES on the category-update endpoints. Use exact strings.
Identifier Standards¶
| Table | ID Column | Type | Notes |
|---|---|---|---|
bank_transactions |
id |
bigint (bigserial) |
|
bank_statements |
statement_id |
text |
{bank_slug}{type_slug}{last4}_{YYYY-MM-DD} |
orders |
order_id |
text |
Vendor order number, normalized by _normalize_order_id() |
order_items |
id |
integer (serial) |
|
receipts |
id |
text |
UUID v4 |
receipt_items |
id |
text |
UUID v4 |
accounts |
id |
integer (serial) |
|
account_balances |
id |
bigint (bigserial) |
|
spend_budgets, annual_budgets |
id |
bigint (bigserial) |
Correct pattern for new tables |
mortgage_statements |
id |
bigint (bigserial) |
|
item_category_cache |
cache_key |
text |
ASIN when known, else sha1 of the normalized title |
Rule for new tables: bigserial (preferred) or uuid. Existing inconsistencies are
grandfathered.
orders.order_id has no ON UPDATE
order_items.order_id references orders.order_id with ON DELETE CASCADE and no
ON UPDATE, so Postgres rejects a plain UPDATE of the key. Rewriting an order ID means copy
the row under the new key, repoint the children, drop the old row, in that order.
repair_order_data.py --normalize-ids does exactly this.
Source File Tracking¶
Every ingest pipeline stores the source filename (not the full path) for audit trail.
| Table | Column |
|---|---|
bank_statements |
source_file |
bank_transactions |
source_file |
account_balances |
source_file |
mortgage_statements |
source_file |
receipts |
filename |
statement_files_processed |
original_filename plus file_hash for dedupe |
Account References¶
accounts is the reference table. Its real columns are id, name, nickname, institution,
account_type, account_last4, display_group, is_active, statement_frequency, created_at.
There is still no FK from bank_statements or bank_transactions to accounts. Statements match
accounts by institution + account_last4 + account_type string comparison at ingest time.
account_balances.account_id is the one real FK.
Rule for new tables: reference accounts.id. No more bare account_last4 strings.
Quick Reference: What to Use When¶
| Scenario | Use |
|---|---|
| New table with a date field | date if no time; timestamptz if time matters |
| New table with a money field | numeric(12,2) |
| New table primary key | bigserial |
| Storing ingest source | source_file text (filename only) |
| Category value | Exact string from CATEGORY_ICONS in categories.py |
| Account reference | accounts.id FK |
| Timestamps for created/updated | timestamptz DEFAULT now() |