Skip to content

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 (groceriesFood & Grocery, beautyPersonal Care & Wellness, diyHome 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

  1. Add the key and icon to CATEGORY_ICONS in categories.py
  2. Decide membership in ANNUAL_ONLY_CATS and FIXED_COST_CATS. ITEM_CATEGORIES follows CATEGORIES automatically and needs no edit
  3. Add the same entry to CATEGORIES in receipts/app.py, which keeps its own copy because it is a separate container. Both test suites fail the build if the two disagree
  4. Add aliases to LEGACY_ALIASES if a classifier is likely to reach for a different word
  5. Write reclassification SQL for existing rows that belong in the new category
  6. Update the table above
  7. 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