Skip to content

Commit ad5e9a5

Browse files
authored
fix(billing): include plans without products in plan listings (#1834)
1 parent c4b8ca5 commit ad5e9a5

2 files changed

Lines changed: 37 additions & 25 deletions

File tree

internal/store/postgres/billing_plan_repository.go

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,22 @@ type PlanProductRow struct {
5454
PlanUpdatedAt time.Time `db:"plan_updated_at"`
5555
PlanDeletedAt *time.Time `db:"plan_deleted_at"`
5656

57-
ProductID string `db:"product_id"`
58-
ProductProviderID string `db:"product_provider_id"`
57+
// product columns are pointers because a left join leaves them null for a
58+
// plan that has no products
59+
ProductID *string `db:"product_id"`
60+
ProductProviderID *string `db:"product_provider_id"`
5961
ProductPlanIDs pq.StringArray `db:"product_plan_ids"`
60-
ProductName string `db:"product_name"`
62+
ProductName *string `db:"product_name"`
6163
ProductTitle *string `db:"product_title"`
6264
ProductDescription *string `db:"product_description"`
6365

64-
ProductBehavior string `db:"product_behavior"`
66+
ProductBehavior *string `db:"product_behavior"`
6567
ProductConfig BehaviorConfig `db:"product_config"`
66-
ProductState string `db:"product_state"`
68+
ProductState *string `db:"product_state"`
6769
ProductMetadata types.NullJSONText `db:"product_metadata"`
6870

69-
ProductCreatedAt time.Time `db:"product_created_at"`
70-
ProductUpdatedAt time.Time `db:"product_updated_at"`
71+
ProductCreatedAt *time.Time `db:"product_created_at"`
72+
ProductUpdatedAt *time.Time `db:"product_updated_at"`
7173
ProductDeletedAt *time.Time `db:"product_deleted_at"`
7274
}
7375

@@ -93,18 +95,18 @@ func (pr PlanProductRow) getPlan() (plan.Plan, error) {
9395

9496
func (pr PlanProductRow) getProduct() (product.Product, error) {
9597
prod := Product{
96-
ID: pr.ProductID,
97-
ProviderID: pr.ProductProviderID,
98+
ID: ptrToString(pr.ProductID),
99+
ProviderID: ptrToString(pr.ProductProviderID),
98100
PlanIDs: pr.ProductPlanIDs,
99-
Name: pr.ProductName,
101+
Name: ptrToString(pr.ProductName),
100102
Title: pr.ProductTitle,
101103
Description: pr.ProductDescription,
102-
Behavior: pr.ProductBehavior,
104+
Behavior: ptrToString(pr.ProductBehavior),
103105
Config: pr.ProductConfig,
104-
State: pr.ProductState,
106+
State: ptrToString(pr.ProductState),
105107
Metadata: pr.ProductMetadata,
106-
CreatedAt: pr.ProductCreatedAt,
107-
UpdatedAt: pr.ProductUpdatedAt,
108+
CreatedAt: ptrToTime(pr.ProductCreatedAt),
109+
UpdatedAt: ptrToTime(pr.ProductUpdatedAt),
108110
DeletedAt: pr.ProductDeletedAt,
109111
}
110112

@@ -353,8 +355,9 @@ func (r BillingPlanRepository) List(ctx context.Context, filter plan.Filter) ([]
353355
func (r BillingPlanRepository) ListWithProducts(ctx context.Context, filter plan.Filter) ([]plan.Plan, error) {
354356
pln := goqu.T(TABLE_BILLING_PLANS).As("plan")
355357
prd := goqu.T(TABLE_BILLING_PRODUCTS).As("product")
358+
// a left join keeps plans that have no products; an inner join would drop them
356359
stmt := dialect.From(pln).
357-
Join(
360+
LeftJoin(
358361
prd,
359362
goqu.On(
360363
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
371374
pln.Col("metadata").As("plan_metadata"),
372375
pln.Col("created_at").As("plan_created_at"),
373376
pln.Col("updated_at").As("plan_updated_at"),
374-
prd.Col("deleted_at").As("plan_deleted_at"),
377+
pln.Col("deleted_at").As("plan_deleted_at"),
375378
prd.Col("id").As("product_id"),
376379
prd.Col("provider_id").As("product_provider_id"),
377380
prd.Col("name").As("product_name"),
@@ -437,19 +440,19 @@ func (r BillingPlanRepository) ListWithProducts(ctx context.Context, filter plan
437440
if err != nil {
438441
return nil, err
439442
}
440-
441-
prod, err := row.getProduct()
442-
if err != nil {
443-
return nil, err
443+
if existing, ok := planMap[pln.ID]; ok {
444+
pln = existing
444445
}
445446

446-
planInMap, exists := planMap[pln.ID]
447-
if exists {
448-
planInMap.Products = append(planInMap.Products, prod)
449-
} else {
447+
// a left join gives a null product id for a plan that has no products
448+
if row.ProductID != nil {
449+
prod, err := row.getProduct()
450+
if err != nil {
451+
return nil, err
452+
}
450453
pln.Products = append(pln.Products, prod)
451-
planMap[pln.ID] = pln
452454
}
455+
planMap[pln.ID] = pln
453456
}
454457

455458
plans := []plan.Plan{}

internal/store/postgres/null_converters.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package postgres
33
import (
44
"database/sql"
55
"encoding/json"
6+
"time"
67

78
"github.com/jmoiron/sqlx/types"
89
"github.com/raystack/frontier/pkg/metadata"
@@ -74,6 +75,14 @@ func ptrToString(ptr *string) string {
7475
return ""
7576
}
7677

78+
// ptrToTime safely converts a time pointer to a time.Time, returning zero time if nil
79+
func ptrToTime(ptr *time.Time) time.Time {
80+
if ptr != nil {
81+
return *ptr
82+
}
83+
return time.Time{}
84+
}
85+
7786
// unmarshalNullJSONText unmarshals NullJSONText to map[string]any
7887
func unmarshalNullJSONText(metadata types.NullJSONText) (map[string]any, error) {
7988
if !metadata.Valid {

0 commit comments

Comments
 (0)