Repository navigation
feat(plans): surface legacy no-account plans as 'Unassigned' (backfill not feasible) #973
Description
Summary
Follow-up to #705. The Plans page Account global filter (#715) INNER-JOINs plan_accounts, and #743 made target_accounts required at create time. Plans created before #743 with zero plan_accounts rows ("universal" / legacy no-account plans) are therefore invisible whenever an account filter is active.
The investigated remedy was Option 1: backfill -- associate each legacy no-account plan with its recoverable account(s). After a schema + data-model investigation, backfill is not safely possible, so this issue tracks the recommended alternative instead: surface legacy no-account plans under an "Unassigned" pseudo-account in the filter.
Why backfill is not feasible
A deterministic, recoverable account source for these plans does not exist on any migrated database:
- The plans are already hard-deleted. Migration
000060_cleanup_universal_plans(shipped in fix(db): duplicate migration version 000057 on base (cleanup_universal_plans vs drop_user_role_to_groups) #970/fix(db): renumber duplicate migration 000057_cleanup_universal_plans to 000060 (closes #970) #971) runsDELETE FROM purchase_plans WHERE NOT EXISTS (SELECT 1 FROM plan_accounts ...). Every legacy no-account plan is permanently removed by the time any later migration could run, and its.down.sqlis a documented no-op (data cannot be reconstructed from SQL). A new backfill migration (next free slot000065) would match zero rows. - The plan->account link is severed on delete.
purchase_executions.plan_idandpurchase_history.plan_idare bothON DELETE SET NULL. Even though those child tables carry acloud_account_id(added in000011), once 000060 deletes the plan theirplan_idbecomes NULL, so an orphaned execution/history row can no longer be attributed back to any specific plan. (a) is therefore impossible. - No other plan->account reference exists.
purchase_planshas no account column in any migration (000001...000064);plan_accountsis the sole linkage. The plan'sservicesJSONB has no account. (b) is impossible. - Single-account fan-out is unsafe. Assigning every legacy plan to "the one cloud account" only holds in single-account deployments and would silently mis-scope plans in multi-account deployments -- the exact concern that drove 000060 to delete rather than fan-out (see ops(plans): clean up existing universal plans (DB rows with no plan_accounts entry) #742). Guessing associations is explicitly out of scope.
Inserting a backfill migration before 000060 is also impossible: 000060 is already applied on deployed DBs, and golang-migrate does not permit out-of-order versions.
Recommended alternative: "Unassigned" surfacing
Rather than guessing associations, make legacy/unscoped plans discoverable without an account chip selection:
- Backend
getPlans(): when an account filter is active, return plans matching the selected accounts via theplan_accountsjoin plus plans that have zeroplan_accountsrows (LEFT JOIN +pa.account_id IS NULL), grouped under a synthetic "Unassigned" bucket. - Frontend Plans page: show an "Unassigned" group/badge so operators can find and re-scope these plans manually.
Note: on databases already migrated past 000060 there are no zero-account plans left, so this primarily protects DBs that predate the upgrade and any future regressions that reintroduce unscoped plans. It is the safe, non-destructive complement to the 000060 cleanup.
References
- bug(plans): Account global filter on Plans page is non-functional (plans-have-no-account) #705 (Account filter non-functional on Plans page)
- fix(plans): honor Account global filter via plan_accounts join #715 (filter via plan_accounts join)
- fix(plans): eliminate universal plans — require target_accounts on creation #743 (require target_accounts on creation)
- ops(plans): clean up existing universal plans (DB rows with no plan_accounts entry) #742 / fix(db): duplicate migration version 000057 on base (cleanup_universal_plans vs drop_user_role_to_groups) #970 / fix(db): renumber duplicate migration 000057_cleanup_universal_plans to 000060 (closes #970) #971 (universal-plan cleanup migration 000060)
- QA sheet rows 325/326
Activity
test(plans): real DB test for Unassigned bucket query (refs #973)
feat(plans): surface legacy no-account plans as Unassigned (closes #973…