Budget System

Overview

Budget management with envelope-based tracking, automatic transaction mapping from bank imports, vendor bill integration via Billingo, and multi-currency support.

Envelope Types

Four envelope types defined in BudgetEnvelopeType:

TypePurposeDisplay
BUDGETPeriodic spending limitsProgress bar (spent vs target)
SAVINGS_GOALOne-time savings targetsProgress bar (saved vs target)
SAVINGS_TARGETRecurring savings targetsProgress bar per period
TRACKINGBalance monitoring, no targets30-day sparkline + balance

Cross-Account Envelopes

  • account_id is nullable - NULL means cross-account
  • Cross-account envelopes accept transactions from any bank account
  • Displayed with globe icon, converted balances using cached exchange rates

Transaction Types & Sources

BudgetTransactionType = 'INCOME' | 'EXPENSE' | 'TRANSFER'
BudgetTransactionSource = 'MANUAL' | 'INVOICE' | 'TIMESHEET' | 'BANK' | 'VENDOR_BILL'
  • ext JSONB column stores metadata from external sources (bank imports)
  • budget_transaction_amount_check constraint: amount > 0

Mapping Rules

Automatic transaction categorization from bank imports:

FieldTypePurpose
account_idsUUID[]Which bank accounts this rule applies to (array, GIN-indexed)
priorityintLower = higher priority, deterministic application order
conditionsJSONBMatch criteria against transaction ext fields
actionsJSONBEnvelope assignment, categorization

Hooks

  • useBudgetMappingRules(accountId?) - Fetch rules, ordered by priority
  • useAvailableExtKeys(accountId) - Extract distinct keys from ext JSONB (limited to 100 txns)

UI

  • Conditions/actions rule builder
  • Preview sidebar (MappingPreviewSidebar.tsx)
  • Full bilingual support (42 HU translation keys)

Vendor Bills (Billingo Integration)

Synced via billingo-sync-inbound edge function:

Data Flow

Billingo API → edge function → vendor_bill table → budget_transaction (for paid bills)

Field Mappings (learned the hard way)

Our fieldBillingo API field
vendor_namespending.partner?.name
total_grossspending.total_gross
spending_datespending.fulfillment_date
paid_datespending.paid_at
statusDerived from paid_at + due_date

Status Derivation

function deriveStatus(spending): 'PAID' | 'OVERDUE' | 'OUTSTANDING' {
  if (spending.paid_at) return 'PAID'
  if (spending.due_date && new Date(spending.due_date) < now) return 'OVERDUE'
  return 'OUTSTANDING'
}

Schema Notes

  • total_gross is NUMERIC (not INTEGER) - API returns decimals
  • Budget transactions filtered: (bill.total_gross ?? 0) > 0 to skip zero-amount bills

UI Components

ComponentFilePurpose
BudgetOverviewBudgetOverview.tsxEnvelope cards grid with type-aware rendering
EnvelopeDetailSheetEnvelopeDetailSheet.tsxSide panel with transactions and summary
VendorBillsVendorBills.tsxVendor bills table with sync and status badges
MappingPreviewSidebarMappingPreviewSidebar.tsxRule testing preview

Status Badges

  • Color-coded: green=PAID, red=OVERDUE, yellow=OUTSTANDING
  • Record-based lookups for status→CSS class mapping (not switch statements)
  • Translated labels via STATUS_LABEL_KEY Record

Envelope Cards

  • Sorted by type group (BUDGET → SAVINGS → TRACKING), then alphabetical
  • TRACKING envelopes: 30-day sparkline (daily balance snapshots, computed client-side)
  • Cross-account envelopes: globe icon indicator

Exchange Rates

  • budget_exchange_rate table: from_currency, to_currency, rate, fetched_at
  • Fetched by edge function from ECB/exchangerate.host
  • Cached client-side via React Query
  • CurrencyProvider provides HUF/EUR context