Skip to content

feat(plans): surface legacy no-account plans as Unassigned (closes #973) - #994

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/973-unassigned-plans
Jun 6, 2026
Merged

cristim merged 2 commits into
feat/multicloud-web-frontendfrom
feat/973-unassigned-plans

Conversation

@cristim

@cristim cristim commented Jun 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Legacy plans created before target_accounts was required (fix(plans): eliminate universal plans — require target_accounts on creation #743) have zero plan_accounts rows and are silently excluded from account-filtered views (the INNER JOIN on plan_accounts drops them entirely)
  • Backend: buildListPlansQuery now uses LEFT JOIN + OR NOT EXISTS to include zero-account plans alongside matched-account plans; a computed unassigned boolean column tags them so callers can bucket the two groups without a second query
  • Frontend: renderPlans splits plans into assigned/unassigned buckets; unassigned plans render under a clearly labeled "Unassigned" section (read-only: account-scoped actions suppressed, History/Delete remain)

Backend approach

buildListPlansQuery adds false AS unassigned in the no-filter path (all plans returned), and in the filtered path:

  • LEFT JOIN plan_accounts instead of INNER JOIN
  • WHERE pa.account_id IN (...) OR NOT EXISTS (SELECT 1 FROM plan_accounts WHERE plan_id = pp.id)
  • (NOT EXISTS ...) AS unassigned as a computed column

PurchasePlan gains an Unassigned bool field (json:"unassigned,omitempty"). Tenant/permission scoping is unchanged -- the existing requirePermission + requirePlanAccess gates apply to listPlans as before.

Frontend bucket location

An <div class="plans-section-header unassigned-plans-header"> section is appended after all assigned plan cards. Unassigned plan cards omit the Add Purchases, Edit, and enable toggle actions (they have no account scope to purchase against). History and Delete remain available.

Tests

Backend:

  • TestPGXMock_ListPurchasePlans_UnassignedIncluded -- zero-account plan flagged Unassigned=true, assigned plan flagged false
  • TestHandler_HandleRequest_ListPlans_UnassignedFlagged -- API-level regression guard: both plans appear in response with correct unassigned values

Frontend:

  • unassigned plan renders under Unassigned section, assigned plan does not (issue #973) -- asserts section header present and ordering correct
  • no Unassigned section when all plans are assigned -- asserts header absent

Test plan

  • Go build passes: go build ./...
  • Config tests green: go test ./internal/config/... (560 pass)
  • API tests green: go test ./internal/api/... (1465 pass)
  • Frontend build passes: npm run build
  • Frontend tests green: npm test (2360 pass, 1 pre-existing skip)
  • Seed a DB plan with zero plan_accounts rows, select an account filter, verify plan appears under "Unassigned" in the UI
  • Verify assigned plans still appear under their account normally

closes #973

🤖 Generated with claude-flow

Plans created before target_accounts was required (#743) have zero rows
in plan_accounts and are invisible in account-filtered views because the
JOIN on plan_accounts excludes them.

Backend: buildListPlansQuery now uses LEFT JOIN + OR NOT EXISTS so that
zero-account plans are included alongside matched-account plans when an
account filter is active. A computed boolean column "unassigned" (true
for zero-account plans, false otherwise) is selected so callers can
bucket the two groups without a second query. The no-filter case
continues to return all plans and sets unassigned=false. PurchasePlan
gains an Unassigned field that is omitted from JSON when false.

Frontend: renderPlans splits plans into assigned and unassigned buckets.
Assigned plans render as before. Unassigned plans are appended under a
clearly labeled "Unassigned" section header (class
unassigned-plans-header). Account-scoped actions (Add Purchases, Edit,
enable toggle) are suppressed for unassigned plans; History and Delete
remain available. Account-name resolution is skipped for unassigned
plans because they have no plan_accounts rows.

Tests: backend adds TestPGXMock_ListPurchasePlans_UnassignedIncluded
(zero-account plan flagged true, assigned plan flagged false) and
TestHandler_HandleRequest_ListPlans_UnassignedFlagged (API-level
regression guard). Frontend adds two loadPlans tests: one asserting the
Unassigned section appears with the correct order, another asserting it
is absent when all plans are assigned.
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/low Minor harm urgency/this-quarter Within the quarter impact/few Limited audience type/bug Defect labels Jun 5, 2026
@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d3fa27d-385e-4458-938b-db9359bb141e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/973-unassigned-plans

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim

cristim commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Add a testcontainers-backed integration test
(TestPostgresStoreDB_ListPurchasePlans_UnassignedBucket) that exercises
the LEFT JOIN + OR NOT EXISTS query introduced in #973 against a real
Postgres instance.

Seed layout:
- planA assigned to accountX: must appear with Unassigned=false
- planB with zero plan_accounts rows: must appear with Unassigned=true
- planC assigned to accountY only: must NOT appear in accountX filter

The discriminating assertion (planB present in accountX-filtered result)
fails on the pre-fix INNER JOIN code and passes with the LEFT JOIN fix,
so a regression back to INNER JOIN will be caught by CI.

Also fix two pre-existing compile errors in store_postgres_test.go
(package config_test): add the config. qualifier to PurchasePlanFilter
and inline the unexported pf() helper that was inaccessible from the
external test package.
@cristim

cristim commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit eec7a39 into feat/multicloud-web-frontend Jun 6, 2026
6 checks passed
@cristim
cristim deleted the feat/973-unassigned-plans branch July 27, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

impact/few Limited audience priority/p2 Backlog-worthy severity/low Minor harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant