Spending Category Taxonomy¶
Last Updated: 2026-08-11
Source of truth: heezy-containers/dockerfiles/heezy-finance/categories.py
The taxonomy lives in categories.py, not app.py. Both app.py and item_classifier.py import
from it so the two lists cannot drift. They did drift once (commit a05a99e diverged the
classifier's copy for three months), which is why the module exists.
Canonical Categories¶
27 categories. Every category any classifier can emit must be a key in CATEGORY_ICONS.
| Category | Icon | Covers | Notes |
|---|---|---|---|
| Alcohol | 🍺 | Beer, wine, liquor | Split out of Food & Grocery |
| Automotive | 🚗 | Repairs, parts, registration, service | Annual-budget only |
| Clothing | 👕 | Apparel, shoes | |
| Donations | 🙏 | Charitable giving | Annual-budget only |
| Electronics | 💻 | Devices, tech gear, accessories | Annual-budget only |
| Entertainment | 🎮 | Games, events, concerts, leisure | |
| Food & Grocery | 🛒 | Groceries, Costco runs, household consumables | Not restaurant meals |
| Gas & Fuel | ⛽ | Fuel purchases | |
| Gifts | 🎁 | Presents, holidays | Annual-budget only |
| Health Care Costs | 🏥 | Medical bills, prescriptions, doctor/dentist, copays | Not haircuts |
| Hockey | 🏒 | GRAHA, KIIS, Pure Hockey, ice time, equipment | Annual-budget only. Hockey travel stays in Travel |
| Home Maintenance | 🔧 | Repairs, contractors, tools, hardware | Split out of Household |
| Household | 🏠 | Furniture, decor, living purchases | Not mortgage, not utilities |
| Insurance | 🛡️ | Auto, home, life premiums | Account-level, never a line item |
| MittenTech | 🖥️ | Mitten Tech Consulting business expenses | |
| Mortgage | 🏦 | Fifth Third mortgage payments | Excluded from spend totals |
| Other | 📦 | Uncategorized catch-all | |
| Personal Care & Wellness | 💅 | Haircuts, nails, salon, grooming, spa, gym | Renamed from Personal Care |
| Pet Care | 🐾 | Vet bills, pet food, litter, supplies | Implemented, no longer backlogged. Annual-budget only |
| Restaurant | 🍽️ | Dining out, takeout, delivery | |
| Retirement Funds | 🏦 | IRA and 401k contributions | Excluded from spend totals |
| Sports | 🏅 | Non-hockey sports and outdoors | |
| Stock Funds | 📈 | Brokerage contributions | Excluded from spend totals |
| Subscription | 📱 | Streaming, software, recurring services | |
| Transportation | 🚕 | Parking, tolls, rideshare, transit | Added 2026-08. Was emitted by scan_statements from the start but missing from the taxonomy, hiding $160.32 across 10 transactions |
| Travel | ✈️ | All travel including hockey trips and vacations | Annual-budget only |
| Utilities | 💡 | Consumers Energy, DTE, Xfinity/Comcast | Fixed cost |
Category Subsets¶
categories.py defines four groups. Adding a category means deciding which of these it belongs to.
| Set | Members | Effect |
|---|---|---|
CATEGORIES |
all 27, sorted | Dropdown population, validation on category-update endpoints |
ANNUAL_ONLY_CATS |
Automotive, Donations, Electronics, Gifts, Hockey, Pet Care, Travel | Budgeted yearly on the Budget page, never in the monthly table |
FIXED_COST_CATS |
Mortgage, Utilities | Rendered in the fixed/variable costs card, not the main budget table |
ITEM_CATEGORIES |
all 27, identical to CATEGORIES |
The values the item classifier may emit. Deliberately the same set, see below |
ITEM_CATEGORIES is no longer a subset
It was 18 of the 27 until 2026-08-12, excluding account-level concepts (Mortgage, Insurance,
Retirement Funds) on the reasoning that nobody puts a mortgage in a shopping cart. True of a
cart, but the list is also what the receipts dropdown and the reconcile item editor must agree
with, and two lists drift: Alcohol reached the classifier and never reached the receipts UI,
so a Costco receipt of beer had to be filed as Automotive.
One taxonomy now spans items, receipts, bank transactions and budget lines, so consolidation
never translates between vocabularies. The cost is that the classifier is offered account-level
categories and a model answer of Mortgage for a widget will stick, where the narrow list forced
Other. normalize_category still rejects anything outside the taxonomy. If that shows up in
practice, the fix is prompt guidance rather than a second list.
Both services carry a test that parses the other's list and fails the build on any difference.
Legacy Aliases¶
LEGACY_ALIASES maps ~40 pre-2026-08 category strings and common LLM near-misses onto the canonical
set (groceries → Food & Grocery, beauty → Personal Care & Wellness, diy → Home
Maintenance). Everything a model returns is squeezed through normalize_category() before it goes
near the database. normalize_category() returns None when it cannot resolve a string, so callers
choose between falling back to Other and rejecting the classification.
Every emitted category must exist in CATEGORY_ICONS
/api/overview builds its comparison rows by walking CATEGORIES while the header totals sum
every transaction. A category present in the database but absent from CATEGORY_ICONS is spent
money that appears in no row, and the two numbers silently disagree. That is exactly what
Transportation did. test_merchant_normalization.py now asserts the classifier and the taxonomy
stay in step.
No FK enforcement
There is no categories lookup table. Category values are free text in bank_transactions,
receipt_items, and order_items. Use the exact strings above when writing data or migrations.
Adding a New Category¶
- Add the key and icon to
CATEGORY_ICONSincategories.py - Decide membership in
ANNUAL_ONLY_CATSandFIXED_COST_CATS.ITEM_CATEGORIESfollowsCATEGORIESautomatically and needs no edit - Add the same entry to
CATEGORIESinreceipts/app.py, which keeps its own copy because it is a separate container. Both test suites fail the build if the two disagree - Add aliases to
LEGACY_ALIASESif a classifier is likely to reach for a different word - Write reclassification SQL for existing rows that belong in the new category
- Update the table above
- Open a PR in
heezy-containers; branch protection blocks pushing to main directly
Change History¶
| Date | Change |
|---|---|
| 2026-08-11 | Added Transportation (was emitted but untracked, hid $160.32) |
| 2026-08 | Taxonomy moved from app.py to categories.py; item_classifier.py shares it |
| 2026-08 | Added Donations, Gifts, Sports, Alcohol, Automotive, Insurance, Home Maintenance, Retirement Funds, Stock Funds, MittenTech |
| 2026-08 | Renamed Personal Care to Personal Care & Wellness |
| 2026-08 | Pet Care implemented (was backlogged since June) |
| 2026-06-19 | Split Health & Personal Care into Health Care Costs and Personal Care |
| 2026-06-19 | Split Household into Household, Mortgage, and Utilities |
| 2026-06-19 | Added Hockey |