Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 43 additions & 16 deletions internal/config/store_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,9 +584,20 @@ func (s *PostgresStore) DeletePurchasePlan(ctx context.Context, planID string) e
}

// buildListPlansQuery returns the SQL query and args for ListPurchasePlans.
// When accountIDs is non-empty the query JOINs plan_accounts and filters
// on account_id IN ($1, $2, …) using parameterised placeholders so the
// result is bounded to plans that reference at least one of the given accounts.
//
// When accountIDs is empty, all plans are returned.
//
// When accountIDs is non-empty, the result includes two categories of plans:
// 1. Plans explicitly linked to at least one of the given accounts via
// plan_accounts (targeted plans).
// 2. Plans with NO rows in plan_accounts at all (universal plans, i.e. the
// Target Account field was left blank, meaning "all accounts of this
// provider"). A universal plan is included only when at least one of the
// given accounts has a provider that appears in the plan's services JSONB
// (each service value carries a "provider" field).
//
// The accountIDs slice is passed as a single $1 Postgres array argument so
// that ANY($1) can be used in both sub-queries without repeating parameters.
func buildListPlansQuery(accountIDs []string) (query string, args []any) {
if len(accountIDs) == 0 {
return `
Expand All @@ -597,27 +608,43 @@ func buildListPlansQuery(accountIDs []string) (query string, args []any) {
ORDER BY created_at DESC
`, nil
}
placeholders := make([]string, len(accountIDs))
args = make([]any, len(accountIDs))
for i, id := range accountIDs {
placeholders[i] = fmt.Sprintf("$%d", i+1)
args[i] = id
}
query = fmt.Sprintf(`
SELECT DISTINCT pp.id, pp.name, pp.enabled, pp.auto_purchase, pp.notification_days_before,
// A single array argument lets both sub-queries reuse $1.
args = []any{accountIDs}
query = `
SELECT pp.id, pp.name, pp.enabled, pp.auto_purchase, pp.notification_days_before,
pp.services, pp.ramp_schedule, pp.created_at, pp.updated_at,
pp.next_execution_date, pp.last_execution_date, pp.last_notification_sent
FROM purchase_plans pp
JOIN plan_accounts pa ON pa.plan_id = pp.id
WHERE pa.account_id IN (%s)
WHERE pp.id IN (
-- targeted plans: explicitly linked to one of the given accounts
SELECT pa.plan_id
FROM plan_accounts pa
WHERE pa.account_id = ANY($1::uuid[])

UNION

-- universal plans: no target accounts set, provider matches
SELECT pp2.id
FROM purchase_plans pp2
WHERE NOT EXISTS (
SELECT 1 FROM plan_accounts pa2 WHERE pa2.plan_id = pp2.id
)
AND EXISTS (
SELECT 1
FROM cloud_accounts ca
JOIN jsonb_each(pp2.services) AS svc(k, v) ON (v->>'provider') = ca.provider
WHERE ca.id = ANY($1::uuid[])
)
)
ORDER BY pp.created_at DESC
`, strings.Join(placeholders, ", "))
`
return query, args
}

// ListPurchasePlans lists purchase plans, optionally filtered by account IDs.
// When filter.AccountIDs is non-empty the result is limited to plans that
// reference at least one of those accounts via the plan_accounts join table.
// When filter.AccountIDs is non-empty the result includes plans that reference
// at least one of those accounts (via plan_accounts) and universal plans (no
// target accounts) whose provider matches any of the given accounts.
func (s *PostgresStore) ListPurchasePlans(ctx context.Context, filter PurchasePlanFilter) ([]PurchasePlan, error) {
query, args := buildListPlansQuery(filter.AccountIDs)
rows, err := s.db.Query(ctx, query, args...)
Expand Down
55 changes: 55 additions & 0 deletions internal/config/store_postgres_pgxmock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,61 @@ func TestPGXMock_ListPurchasePlans_Error(t *testing.T) {
require.Error(t, err)
}

// TestPGXMock_ListPurchasePlans_AccountFilter verifies that when AccountIDs is
// set the query passes a single array arg (the $1 ANY-placeholder used by
// both sub-queries) and returns the rows the DB produces. The SQL shape is
// tested at the query level; end-to-end provider-match logic requires a real
// Postgres instance and is covered by the DB-integration test suite.
func TestPGXMock_ListPurchasePlans_AccountFilter(t *testing.T) {
mock := newMock(t)
store := storeWith(mock)
ctx := context.Background()

accountID := "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
now := time.Now().Truncate(time.Second)
svcJSON, _ := json.Marshal(map[string]ServiceConfig{
"aws:rds": {Provider: "aws", Service: "rds"},
})
rampJSON, _ := json.Marshal(RampSchedule{})
cols := []string{
"id", "name", "enabled", "auto_purchase", "notification_days_before",
"services", "ramp_schedule", "created_at", "updated_at",
"next_execution_date", "last_execution_date", "last_notification_sent",
}
rows := pgxmock.NewRows(cols).
AddRow("p1", "Targeted Plan", true, false, 3, svcJSON, rampJSON, now, now,
sql.NullTime{}, sql.NullTime{}, sql.NullTime{}).
AddRow("p2", "Universal AWS Plan", true, false, 7, svcJSON, rampJSON, now, now,
sql.NullTime{}, sql.NullTime{}, sql.NullTime{})

// The refactored query passes a single array arg for $1.
mock.ExpectQuery("SELECT").WithArgs(pgxmock.AnyArg()).WillReturnRows(rows)

plans, err := store.ListPurchasePlans(ctx, PurchasePlanFilter{AccountIDs: []string{accountID}})
require.NoError(t, err)
assert.Len(t, plans, 2)
assert.Equal(t, "Targeted Plan", plans[0].Name)
assert.Equal(t, "Universal AWS Plan", plans[1].Name)
assert.NoError(t, mock.ExpectationsWereMet())
}

// TestPGXMock_ListPurchasePlans_AccountFilterError verifies error propagation
// when the DB rejects the account-filtered query.
func TestPGXMock_ListPurchasePlans_AccountFilterError(t *testing.T) {
mock := newMock(t)
store := storeWith(mock)
ctx := context.Background()

mock.ExpectQuery("SELECT").WithArgs(pgxmock.AnyArg()).
WillReturnError(errors.New("db error"))

_, err := store.ListPurchasePlans(ctx, PurchasePlanFilter{
AccountIDs: []string{"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to list purchase plans")
}

// ─── UpdatePurchasePlan ───────────────────────────────────────────────────────

func TestPGXMock_UpdatePurchasePlan_NotFound(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,9 @@ type RecommendationFilter struct {

// PurchasePlanFilter parameterises ListPurchasePlans. Zero-value means "no
// filter" (all plans are returned). Non-empty AccountIDs restricts the result
// to plans that reference at least one of the given account IDs via the
// plan_accounts join table.
// to: (a) plans that reference at least one of the given account IDs via
// plan_accounts, and (b) plans with no target accounts (universal plans)
// whose provider matches any of the given accounts.
type PurchasePlanFilter struct {
AccountIDs []string // nil/empty = all plans
}
Expand Down
Loading