Skip to content

A pick or budget saved from a page loaded before a rename or delete lands under a name no category has #254

Open

Description

@smilne3

What is wrong

Picks (transactions.user_category) and budgets (budgets.category) point at a category by its name, and nothing in the database ties that name to a category. PR #253 (migration 023) makes a rename or a delete move the category's picks and budget all or nothing. Two other writers can still save the old name afterwards.

  • A Budgets page loaded before the rename. app/api/budgets/route.ts:17-25 accepts any non-empty category name. components/BudgetEditor.tsx:109 sends every name the page loaded. So Save, on a Budgets page opened before "Pets" was renamed to "Animals", upserts a budget under "Pets" again.
  • A pick racing a rename or a delete. app/api/transactions/categorize/route.ts reads the category list (step 5, :92-101), then writes the pick in a separate request (step 6, :113). A rename or delete that commits between the two leaves the pick under the old name.

Consequence

  • The stale budget. Budgets doesn't show it, but the Dashboard's "budgeted" total adds up every budget row (app/(app)/dashboard/page.tsx:261-263). So the category's limit counts twice. With illustrative figures, a $100 limit renamed and then saved from the stale page makes the total $200.
  • The racing pick counts as spending under a name no category has, whatever kind the category was, so an income pick moves into spending. The window is the gap between two requests, so this is rare.
  • Both can be found with db/seeds/023_category_rename_delete_checks.sql from PR Renaming or deleting a category is all or nothing, and names the app uses are refused (#100, #117) #253.

How it was found

The SQL review of PR #253. Both were replayed in PGlite with migrations 001 to 023 and made-up rows. The budget case was also confirmed by reading the route and BudgetEditor.

Suggested fix

Tie the names to categories in the database, with composite foreign keys onto categories (household_id, name), which is unique (007):

  • transactions (household_id, user_category) references categories (household_id, name) on update cascade on delete set null (user_category). The column list on set null needs Postgres 15 or later.
  • budgets (household_id, category) references categories (household_id, name) on update cascade on delete cascade.

A stale write then fails with 23503. The two routes could answer that with "Showing the latest" and a reload. A rename or delete would also move picks and budgets by itself, so 023's functions could shrink to the category write.

Before adding the keys, clear the leftovers the 023 checks query lists, or add the keys not valid and validate them later.

A smaller fix for budgets alone: refuse a name the household doesn't have, with a 409, which makes BudgetEditor reload.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions