What
BillingPlanRepository.ListWithProducts (internal/store/postgres/billing_plan_repository.go, the join near line 357) INNER JOINs billing_products on plan.id = ANY(product.plan_ids). A plan with no products never appears in the result. UpsertPlans allows creating a plan with an empty product set, so such plans can exist.
This affects both FrontierService.ListPlans and the new AdminService.ListAllPlans. The point of ListAllPlans (and the future BillingPlan reconcile export) is to surface every plan, including misconfigured or product-less ones, so this is a correctness gap for that endpoint.
Fix
Change the INNER JOIN to a LEFT JOIN and tolerate NULL product columns:
PlanProductRow's product fields are non-nullable today (string, time.Time, BehaviorConfig, pq.StringArray), so scanning NULLs from a LEFT JOIN fails. The fields need to become nullable, or the SELECT needs COALESCE, and the row loop must skip appending an empty product.
- This is a shared read path used by
ListPlans too, so its behavior changes as well (product-less active plans start appearing).
- Ship it with a Postgres/integration test for a product-less plan. No seed today creates one.
Context
Deferred from the #1830 review to keep that PR focused and low-risk. The rework touches shared read code and cannot be unit-tested without Postgres, and the BillingPlan reconciler that needs this does not exist yet.
What
BillingPlanRepository.ListWithProducts(internal/store/postgres/billing_plan_repository.go, the join near line 357) INNER JOINsbilling_productsonplan.id = ANY(product.plan_ids). A plan with no products never appears in the result.UpsertPlansallows creating a plan with an empty product set, so such plans can exist.This affects both
FrontierService.ListPlansand the newAdminService.ListAllPlans. The point ofListAllPlans(and the future BillingPlan reconcile export) is to surface every plan, including misconfigured or product-less ones, so this is a correctness gap for that endpoint.Fix
Change the INNER JOIN to a LEFT JOIN and tolerate NULL product columns:
PlanProductRow's product fields are non-nullable today (string,time.Time,BehaviorConfig,pq.StringArray), so scanning NULLs from a LEFT JOIN fails. The fields need to become nullable, or the SELECT needsCOALESCE, and the row loop must skip appending an empty product.ListPlanstoo, so its behavior changes as well (product-less active plans start appearing).Context
Deferred from the #1830 review to keep that PR focused and low-risk. The rework touches shared read code and cannot be unit-tested without Postgres, and the BillingPlan reconciler that needs this does not exist yet.