Skip to content

Add income tracking Phase 3: net budget math + income edit/delete - #46

Merged
derekl-beep merged 1 commit into
mainfrom
claude/income-phase3
Jul 9, 2026
Merged

Add income tracking Phase 3: net budget math + income edit/delete#46
derekl-beep merged 1 commit into
mainfrom
claude/income-phase3

Conversation

@derekl-beep

Copy link
Copy Markdown
Owner

Summary

Phase 3 of income tracking — closes the two pieces deliberately deferred out of Phase 2 (#45): net budget math for reimbursed expenses, and the ability to edit/delete income entries at all.

  • Net budget math: get_budget_status's "spent" figure now nets each expense against any income linked to it via reimburses_expense_id, clipped at 0 per-expense so an over-reimbursed expense can't drag a category negative (e.g. a $60 dinner with a $30 linked reimbursement now counts as $30 spent against the Dining budget, not $60).
  • Income edit/delete: update_income/delete_income (hard delete — unlike expenses, nothing else references income.id as a foreign key, so there's no soft-delete need), matching chat tools, and PATCH/DELETE /income/{id} endpoints mirroring the existing expense pattern. update_income deliberately never touches reimburses_expense_id — that stays link_income_to_expense's exclusive job, keeping the two tools non-overlapping.
  • Net cash-flow stat: a small "Net" figure (all-time income − expenses) next to the existing "Total" in the Expenses/Income toggle header, computed client-side from data already loaded — no new fetch.

Deliberately not added: a get_cash_flow chat tool or /cashflow slash command. This repo's own roadmap already flags the analytics tool/command surface as possibly over-built (worth assessing real usage before investing further), so the net figure stays UI-only rather than adding a 13th query tool.

Review

Ran this repo's three-angle adversarial review pass (line-by-line, cross-file/removed-behavior, CLAUDE.md conventions) against the diff before committing. Confirmed: no SQL injection risk in the new dynamic queries, no dangling references from delete_income's hard delete (nothing else FKs to income.id, so a deleted reimbursement correctly and automatically un-reimburses its expense on the next read), the frontend netCashFlow figure correctly recomputes after any chat-driven save (both income/expenses props refetch together), and the id-collision guidance introduced in earlier phases still holds without contradiction.

Two real findings, both fixed:

  • The SYSTEM prompt said "four tools split cleanly by table" but actually names five (update_expense, delete_expense, update_income, delete_income, link_income_to_expense) — miscount corrected.
  • get_budget_status's netting is deliberately unscoped by the reimbursement's own date (it ties to the expense's month, regardless of when the income was recorded or linked) and deliberately differs from get_category_breakdown/get_monthly_trend/etc., which still report gross amounts actually paid. Neither is a bug — the budget figure should reflect true net out-of-pocket cost, while historical-spend tools should reflect what was actually paid at the time — but nothing explained the distinction, so a user could get two different numbers for the same transactions with the model having no way to explain why. Added a paragraph to the SYSTEM prompt covering both points.

Test plan

  • uv run pytest tests/ — 150 passed (includes new tests for the netting math, over-reimbursement clipping, update_income/delete_income)
  • npm run lint / npm run build — clean
  • npx playwright test e2e/cashflow.spec.js — 2/2 (Net figure correct and stable across the Expenses/Income toggle)
  • Full regression: npm run test:e2e — 68/68, no regressions

Roadmap / future work

Income tracking (Phases 1–3) is now feature-complete per the original plan. Nothing further currently tracked for this feature.


Generated by Claude Code

Closes the last two deferred pieces from the income tracking plan:

- get_budget_status now nets a category's spent total against any
  income linked to its expenses via reimburses_expense_id, clipped at
  0 per-expense so an over-reimbursed expense can't drag a category
  negative. This differs deliberately from get_category_breakdown and
  the other historical-spend tools, which still report gross amounts
  actually paid - the SYSTEM prompt now explains the distinction so
  the model doesn't get caught flat-footed if a user notices the two
  don't match for the same transactions.
- update_income/delete_income (hard delete - income has no FK
  dependents, unlike expenses) with matching tools and PATCH/DELETE
  /income/{id} endpoints. update_income deliberately never touches
  reimburses_expense_id, keeping that link_income_to_expense's
  exclusive job.
- A "Net" cash-flow stat (all-time income minus expenses) next to the
  existing Total in ExpenseTable's header.

Deliberately did NOT add a cash-flow chat tool/slash command - the
roadmap already flags the analytics command surface as possibly
over-built, so this stays UI-only.

Ran the same three-angle adversarial review as prior phases. Found
and fixed a "four tools" miscount in the SYSTEM prompt (five tools
are actually split by table now) and added the gross-vs-net budget
clarification above. Everything else checked out: parameterized
queries, no dangling references from the hard delete (nothing else
FKs to income.id), netCashFlow correctly recomputes after chat-driven
saves, and the unscoped-by-income-date netting in get_budget_status
is intentional - it ties the reimbursement to the expense's own
economics, not to whenever the income happened to get recorded,
consistent with how update_expense can already retroactively change
a "closed" month's numbers.

Verified: 150 backend tests, 68 e2e tests, lint, and build all green.
@derekl-beep
derekl-beep merged commit 7c37286 into main Jul 9, 2026
3 checks passed
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