Bug: merging or deleting a category leaves dashboard / income-statement totals stale
How are you using Sure?
- I am a self-hosted user (local only)
Self hoster checklist
- Self hosted app commit SHA:
main@ 44bbfb2 - I have confirmed that my app's commit is the latest version of Sure
- Where are you hosting? Other: LXC (Proxmox)
Bug description
Income-statement / dashboard aggregates (IncomeStatement#family_stats, #category_stats, #totals_query, and Transaction::Search#totals) are cached under Family#entries_cache_version, which is derived from entries.count and entries.maximum(:updated_at). That works for every normal write because Entryable declares has_one :entry, touch: true, so saving a Transaction bumps its Entry.
Two paths write transactions.category_id with update_all, which skips callbacks and therefore never touches the entries:
Category::Merger#merge_sources!—family.transactions.where(category_id: source.id).update_all(category_id: target_category.id)(app/models/category/merger.rb:31)Category#replace_and_destroy!—transactions.update_all category_id: replacement&.id(app/models/category.rb:349), used by "Delete category and move transactions to …"
After either operation the cache key is unchanged, so the dashboard keeps serving the pre-merge breakdown — including a slice for a category that no longer exists — until some unrelated Entry is created/updated/deleted for that family.
To Reproduce
- Load the dashboard so the current period's category breakdown is cached (e.g. categories "Food" and "Groceries" both visible).
- Settings → Categories → merge "Groceries" into "Food" (or delete "Groceries" and move its transactions to "Food").
- Transactions list shows the moved rows under "Food".
- Reload the dashboard: the breakdown still shows "Groceries" with its old total and "Food" with its old total.
- Edit any unrelated transaction's name → reload → now correct.
Expected behavior
Category merge and delete-with-replacement invalidate the cached aggregates like any other categorization change.
Proposed fix
Touch the affected transactions' entries after the bulk write in both paths (Entry.where(entryable_type: "Transaction", entryable_id: ids).touch_all inside the same DB transaction), or route the writes through a shared helper on Transaction that does so. Regression tests: entries_cache_version changes after merge and after replace-and-destroy.
Source: we-promise/sure