diff --git a/internal/store/postgres/billing_plan_repository.go b/internal/store/postgres/billing_plan_repository.go index 4dc965eb6..d4062b143 100644 --- a/internal/store/postgres/billing_plan_repository.go +++ b/internal/store/postgres/billing_plan_repository.go @@ -54,20 +54,22 @@ type PlanProductRow struct { PlanUpdatedAt time.Time `db:"plan_updated_at"` PlanDeletedAt *time.Time `db:"plan_deleted_at"` - ProductID string `db:"product_id"` - ProductProviderID string `db:"product_provider_id"` + // product columns are pointers because a left join leaves them null for a + // plan that has no products + ProductID *string `db:"product_id"` + ProductProviderID *string `db:"product_provider_id"` ProductPlanIDs pq.StringArray `db:"product_plan_ids"` - ProductName string `db:"product_name"` + ProductName *string `db:"product_name"` ProductTitle *string `db:"product_title"` ProductDescription *string `db:"product_description"` - ProductBehavior string `db:"product_behavior"` + ProductBehavior *string `db:"product_behavior"` ProductConfig BehaviorConfig `db:"product_config"` - ProductState string `db:"product_state"` + ProductState *string `db:"product_state"` ProductMetadata types.NullJSONText `db:"product_metadata"` - ProductCreatedAt time.Time `db:"product_created_at"` - ProductUpdatedAt time.Time `db:"product_updated_at"` + ProductCreatedAt *time.Time `db:"product_created_at"` + ProductUpdatedAt *time.Time `db:"product_updated_at"` ProductDeletedAt *time.Time `db:"product_deleted_at"` } @@ -93,18 +95,18 @@ func (pr PlanProductRow) getPlan() (plan.Plan, error) { func (pr PlanProductRow) getProduct() (product.Product, error) { prod := Product{ - ID: pr.ProductID, - ProviderID: pr.ProductProviderID, + ID: ptrToString(pr.ProductID), + ProviderID: ptrToString(pr.ProductProviderID), PlanIDs: pr.ProductPlanIDs, - Name: pr.ProductName, + Name: ptrToString(pr.ProductName), Title: pr.ProductTitle, Description: pr.ProductDescription, - Behavior: pr.ProductBehavior, + Behavior: ptrToString(pr.ProductBehavior), Config: pr.ProductConfig, - State: pr.ProductState, + State: ptrToString(pr.ProductState), Metadata: pr.ProductMetadata, - CreatedAt: pr.ProductCreatedAt, - UpdatedAt: pr.ProductUpdatedAt, + CreatedAt: ptrToTime(pr.ProductCreatedAt), + UpdatedAt: ptrToTime(pr.ProductUpdatedAt), DeletedAt: pr.ProductDeletedAt, } @@ -353,8 +355,9 @@ func (r BillingPlanRepository) List(ctx context.Context, filter plan.Filter) ([] func (r BillingPlanRepository) ListWithProducts(ctx context.Context, filter plan.Filter) ([]plan.Plan, error) { pln := goqu.T(TABLE_BILLING_PLANS).As("plan") prd := goqu.T(TABLE_BILLING_PRODUCTS).As("product") + // a left join keeps plans that have no products; an inner join would drop them stmt := dialect.From(pln). - Join( + LeftJoin( prd, goqu.On( goqu.L("CAST(plan.id AS text)").Eq(goqu.L("ANY(product.plan_ids)")), @@ -371,7 +374,7 @@ func (r BillingPlanRepository) ListWithProducts(ctx context.Context, filter plan pln.Col("metadata").As("plan_metadata"), pln.Col("created_at").As("plan_created_at"), pln.Col("updated_at").As("plan_updated_at"), - prd.Col("deleted_at").As("plan_deleted_at"), + pln.Col("deleted_at").As("plan_deleted_at"), prd.Col("id").As("product_id"), prd.Col("provider_id").As("product_provider_id"), prd.Col("name").As("product_name"), @@ -437,19 +440,19 @@ func (r BillingPlanRepository) ListWithProducts(ctx context.Context, filter plan if err != nil { return nil, err } - - prod, err := row.getProduct() - if err != nil { - return nil, err + if existing, ok := planMap[pln.ID]; ok { + pln = existing } - planInMap, exists := planMap[pln.ID] - if exists { - planInMap.Products = append(planInMap.Products, prod) - } else { + // a left join gives a null product id for a plan that has no products + if row.ProductID != nil { + prod, err := row.getProduct() + if err != nil { + return nil, err + } pln.Products = append(pln.Products, prod) - planMap[pln.ID] = pln } + planMap[pln.ID] = pln } plans := []plan.Plan{} diff --git a/internal/store/postgres/null_converters.go b/internal/store/postgres/null_converters.go index 5751188bb..a5e47b5d3 100644 --- a/internal/store/postgres/null_converters.go +++ b/internal/store/postgres/null_converters.go @@ -3,6 +3,7 @@ package postgres import ( "database/sql" "encoding/json" + "time" "github.com/jmoiron/sqlx/types" "github.com/raystack/frontier/pkg/metadata" @@ -74,6 +75,14 @@ func ptrToString(ptr *string) string { return "" } +// ptrToTime safely converts a time pointer to a time.Time, returning zero time if nil +func ptrToTime(ptr *time.Time) time.Time { + if ptr != nil { + return *ptr + } + return time.Time{} +} + // unmarshalNullJSONText unmarshals NullJSONText to map[string]any func unmarshalNullJSONText(metadata types.NullJSONText) (map[string]any, error) { if !metadata.Valid {