Skip to content

feat(budget): report spend the budget view does not count - #40

Open
ryakel wants to merge 1 commit into
developfrom
claude/its-a-plan-setup-tmojxo
Open

feat(budget): report spend the budget view does not count#40
ryakel wants to merge 1 commit into
developfrom
claude/its-a-plan-setup-tmojxo

Conversation

@ryakel

@ryakel ryakel commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Ticket: FATH-10 (backend half — the UI is still to come)

The problem

Budget card actual_amount is computed as SUM(expense_budget_links.amount) — the link amount, not expenses.amount:

SELECT bc.*,
    COALESCE(SUM(ebl.amount), 0) as actual_amount,
    bc.budgeted_amount - COALESCE(SUM(ebl.amount), 0) as remaining_amount
FROM budget_cards bc
LEFT JOIN expense_budget_links ebl ON bc.id = ebl.budget_card_id

So an expense with no link row counts against no budget at all. It shows in the expenses list, feeds /api/user/expenses/summary, and is absent from every budget figure. Linking is optional on every write path — CreateExpenseModal links only if a card was picked, and CSV import links only when the row carried a budget_card_id.

Separately, flights carries fuel_cost, landing_fees, instructor_cost, rental_cost, other_costs. The budget-card system never reads them; only the deprecated budgets endpoint sums them.

Both gaps push reported actuals below true spend, so the budget view reads healthier than the bank account.

What this adds

GET /api/user/budget-cards/summary/uncounted:

  • Unlinked expenses — count, total, and the rows themselves, so the UI can offer to link them rather than just report a number
  • Partially linked expenses — where links exist but don't cover the full expense amount, the shortfall counts against no budget either
  • Flight cost totals, itemised by column, as a separate figure

The two gaps are deliberately kept distinct. An unlinked expense is fixed by linking it. Flight costs are a modelling question — should they become expenses at all? Blending them into one number would imply a single remedy that doesn't exist.

Surfacing only. What actual_amount counts is unchanged.

Testing

  • 116 passed (112 baseline + 4 new), backend unit + integration
  • black 24.10.0, isort 5.13.2, flake8 7.1.1 — the versions pinned in .pre-commit-config.yaml — all clean

One caveat worth stating plainly: the test suite mocks Postgres entirely (tests/conftest.py patches postgres_db.connect), so no test executes the SQL in this PR. The new tests exercise the endpoint contract and the response shape, not the queries. To get some real assurance I parsed all four statements with pglast (libpg_query — the actual Postgres grammar); all four parse. That validates syntax, not that the joins return what I think they do.

Given this repo has now shipped two bugs in files no test executes, that gap seems worth naming rather than glossing.

Still to come on FATH-10

The budget view UI — surfacing these figures next to the totals they contradict, and letting an unlinked expense be linked to a card without leaving the page (reusing the existing link-expense endpoint and expenseStore.linkToBudgetCard).

Note @tanstack/react-query is a dependency with zero usages in src/ — the frontend is Zustand + plain fetch throughout, and the UI half will follow that rather than introduce a second data-fetching pattern.


Generated by Claude Code

Budget card actuals are SUM(expense_budget_links.amount), so an expense
with no link row counts against no budget at all, and flight cost columns
(fuel_cost, landing_fees, instructor_cost, rental_cost, other_costs) are
never read by the card system. Both push reported actuals below true
spend, so the budget view reads healthier than reality.

Adds GET /api/user/budget-cards/summary/uncounted reporting:

- unlinked expenses: count, total, and the rows themselves
- partially linked expenses: where links do not cover the full amount,
  the shortfall counts against no budget either
- flight cost totals, itemised, kept as a separate figure

The two gaps stay distinct deliberately. An unlinked expense is fixed by
linking it; flight costs are a modelling question about whether they
should become expenses at all. Blending them into one number would imply
a single remedy that does not exist.

Surfacing only - what actual_amount counts is unchanged.

SQL validated against the real Postgres grammar via pglast (the test
suite mocks the database, so no test executes these statements).

Ticket: FATH-10

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants