diff --git a/openmeter/productcatalog/plan/adapter/phase.go b/openmeter/productcatalog/plan/adapter/phase.go index 7415ec7c6..f190106b8 100644 --- a/openmeter/productcatalog/plan/adapter/phase.go +++ b/openmeter/productcatalog/plan/adapter/phase.go @@ -154,23 +154,21 @@ func (a *adapter) CreatePhase(ctx context.Context, params plan.CreatePhaseInput) bulk, bulkFn := newRateCardBulkCreate(rateCardInputs, planPhase.ID, params.Namespace) - rateCardRows, err := a.db.PlanRateCard.MapCreateBulk(bulk, bulkFn).Save(ctx) - if err != nil { + if err = a.db.PlanRateCard.MapCreateBulk(bulk, bulkFn).Exec(ctx); err != nil { return nil, fmt.Errorf("failed to bulk create RateCards for PlanPhase %s: %w", planPhase.ID, err) } - planPhase.RateCards = make([]plan.RateCard, 0, len(rateCardRows)) - for _, rateCardRow := range rateCardRows { - if rateCardRow == nil { - return nil, errors.New("invalid query result: nil RateCard received after bulk create") - } - - rateCard, err := fromPlanRateCardRow(*rateCardRow) - if err != nil { - return nil, fmt.Errorf("failed to cast RateCard: %w", err) - } + planPhaseRow, err = a.db.PlanPhase.Query(). + Where(phasedb.Namespace(params.Namespace), phasedb.ID(planPhase.ID)). + WithRatecards(rateCardEagerLoadFeaturesFn). + First(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get PlanPhase: %w", err) + } - planPhase.RateCards = append(planPhase.RateCards, *rateCard) + planPhase, err = fromPlanPhaseRow(*planPhaseRow) + if err != nil { + return nil, fmt.Errorf("failed to cast PlanPhase %w", err) } return planPhase, nil @@ -191,9 +189,15 @@ func newRateCardBulkCreate(r []entdb.PlanRateCard, phaseID string, ns string) ([ SetNillableFeatureKey(r[i].FeatureKey). SetNillableFeaturesID(r[i].FeatureID). SetEntitlementTemplate(r[i].EntitlementTemplate). - SetTaxConfig(r[i].TaxConfig). - SetNillableBillingCadence(r[i].BillingCadence). - SetPrice(r[i].Price) + SetNillableBillingCadence(r[i].BillingCadence) + + if r[i].TaxConfig != nil { + q.SetTaxConfig(r[i].TaxConfig) + } + + if r[i].Price != nil { + q.SetPrice(r[i].Price) + } } } @@ -305,7 +309,7 @@ func (a *adapter) GetPhase(ctx context.Context, params plan.GetPhaseInput) (*pla return nil, errors.New("invalid get PlanPhase parameters") } - query = query.WithRatecards() + query = query.WithRatecards(rateCardEagerLoadFeaturesFn) phaseRow, err := query.First(ctx) if err != nil { @@ -382,34 +386,22 @@ func (a *adapter) UpdatePhase(ctx context.Context, params plan.UpdatePhaseInput) } } - if params.RateCards != nil { - rateCards := make([]plan.RateCard, 0, len(p.RateCards)) - + if params.RateCards != nil && len(*params.RateCards) > 0 { diffResult, err := rateCardsDiff(*params.RateCards, p.RateCards) if err != nil { return nil, fmt.Errorf("failed to generate RateCard diff for PlanPhase update: %w", err) } + if !diffResult.IsDiff() { + return p, nil + } + if len(diffResult.Add) > 0 { bulk, bulkFn := newRateCardBulkCreate(diffResult.Add, p.ID, params.Namespace) - rateCardRows, err := a.db.PlanRateCard.MapCreateBulk(bulk, bulkFn).Save(ctx) - if err != nil { + if err = a.db.PlanRateCard.MapCreateBulk(bulk, bulkFn).Exec(ctx); err != nil { return nil, fmt.Errorf("failed to bulk create RateCards: %w", err) } - - for _, rateCardRow := range rateCardRows { - if rateCardRow == nil { - return nil, errors.New("invalid query result: nil RateCard received after bulk create") - } - - rateCard, err := fromPlanRateCardRow(*rateCardRow) - if err != nil { - return nil, fmt.Errorf("failed to cast RateCard: %w", err) - } - - rateCards = append(rateCards, *rateCard) - } } if len(diffResult.Remove) > 0 { @@ -423,47 +415,39 @@ func (a *adapter) UpdatePhase(ctx context.Context, params plan.UpdatePhaseInput) if len(diffResult.Update) > 0 { for _, rateCardInput := range diffResult.Update { - rateCardRow, err := a.db.PlanRateCard.UpdateOneID(rateCardInput.ID). + q := a.db.PlanRateCard.UpdateOneID(rateCardInput.ID). Where(ratecarddb.Namespace(params.Namespace)). - SetMetadata(rateCardInput.Metadata). + SetOrClearMetadata(&rateCardInput.Metadata). SetName(rateCardInput.Name). - SetNillableDescription(rateCardInput.Description). - SetNillableFeatureKey(rateCardInput.FeatureKey). - SetNillableFeaturesID(rateCardInput.FeatureID). + SetOrClearDescription(rateCardInput.Description). + SetOrClearFeatureKey(rateCardInput.FeatureKey). SetEntitlementTemplate(rateCardInput.EntitlementTemplate). SetTaxConfig(rateCardInput.TaxConfig). - SetNillableBillingCadence(rateCardInput.BillingCadence). - SetPrice(rateCardInput.Price). - Save(ctx) - if err != nil { - return nil, fmt.Errorf("failed to update RateCard: %w", err) - } + SetOrClearBillingCadence(rateCardInput.BillingCadence). + SetPrice(rateCardInput.Price) - if rateCardRow == nil { - return nil, errors.New("invalid query result: nil RateCard received update") + if rateCardInput.FeatureID == nil { + q.ClearFeatureID() } - rateCard, err := fromPlanRateCardRow(*rateCardRow) + err = q.Exec(ctx) if err != nil { - return nil, fmt.Errorf("failed to cast RateCard: %w", err) + return nil, fmt.Errorf("failed to update RateCard: %w", err) } - - rateCards = append(rateCards, *rateCard) } } - if len(diffResult.Keep) > 0 { - for _, rateCardRow := range diffResult.Keep { - rateCard, err := fromPlanRateCardRow(rateCardRow) - if err != nil { - return nil, fmt.Errorf("failed to cast RateCard: %w", err) - } - - rateCards = append(rateCards, *rateCard) - } + p, err = a.GetPhase(ctx, plan.GetPhaseInput{ + NamespacedID: models.NamespacedID{ + Namespace: params.Namespace, + ID: params.ID, + }, + Key: params.Key, + PlanID: params.PlanID, + }) + if err != nil { + return nil, fmt.Errorf("failed to get updated PlanPhase: %w", err) } - - p.RateCards = rateCards } return p, nil @@ -542,7 +526,7 @@ func rateCardsDiff(inputs, rateCards []plan.RateCard) (rateCardsDiffResult, erro } } - // Collect phases to be deleted + // Collect RateCards to be deleted if len(rateCardsVisited) != len(rateCardsMap) { for rateCardKey, rateCard := range rateCardsMap { if _, ok := rateCardsVisited[rateCardKey]; !ok { @@ -554,11 +538,11 @@ func rateCardsDiff(inputs, rateCards []plan.RateCard) (rateCardsDiffResult, erro return result, nil } -func rateCardCmp(r1, r2 entdb.PlanRateCard) (bool, error) { - if r1.ID != r2.ID { - return false, nil - } +func (r rateCardsDiffResult) IsDiff() bool { + return len(r.Add) > 0 || len(r.Update) > 0 || len(r.Remove) > 0 +} +func rateCardCmp(r1, r2 entdb.PlanRateCard) (bool, error) { if r1.Namespace != r2.Namespace { return false, nil } diff --git a/openmeter/productcatalog/plan/adapter/plan.go b/openmeter/productcatalog/plan/adapter/plan.go index 7c636e644..82983170a 100644 --- a/openmeter/productcatalog/plan/adapter/plan.go +++ b/openmeter/productcatalog/plan/adapter/plan.go @@ -414,56 +414,65 @@ func (a *adapter) UpdatePlan(ctx context.Context, params plan.UpdatePlanInput) ( } } - if params.Phases != nil { - phases := make([]plan.Phase, 0, len(p.Phases)) + // Return early if there are no PlanPhases set in Plan. + if params.Phases == nil || len(*params.Phases) == 0 { + return p, nil + } + + // Return early if there are no changes in PlanPhases. + diffResult, err := planPhasesDiff(*params.Phases, p.Phases) + if err != nil { + return nil, fmt.Errorf("failed to calculate Plan Phases diff: %w", err) + } - diffResult := planPhasesDiff(*params.Phases, p.Phases) + if !diffResult.IsDiff() { + return p, nil + } - if len(diffResult.Add) > 0 { - for _, createInput := range diffResult.Add { - createInput.Namespace = params.Namespace + phases := make([]plan.Phase, 0, len(p.Phases)) - phase, err := a.CreatePhase(ctx, createInput) - if err != nil { - return nil, fmt.Errorf("failed to create PlanPhase: %w", err) - } + if len(diffResult.Keep) > 0 { + phases = append(phases, diffResult.Keep...) + } + + if len(diffResult.Add) > 0 { + for _, createInput := range diffResult.Add { + createInput.Namespace = params.Namespace - phases = append(phases, *phase) + phase, err := a.CreatePhase(ctx, createInput) + if err != nil { + return nil, fmt.Errorf("failed to create PlanPhase: %w", err) } + + phases = append(phases, *phase) } + } - if len(diffResult.Remove) > 0 { - for _, deleteInput := range diffResult.Remove { - err = a.DeletePhase(ctx, deleteInput) - if err != nil { - return nil, fmt.Errorf("failed to delete PlanPhase: %w", err) - } + if len(diffResult.Remove) > 0 { + for _, deleteInput := range diffResult.Remove { + err = a.DeletePhase(ctx, deleteInput) + if err != nil { + return nil, fmt.Errorf("failed to delete PlanPhase: %w", err) } } + } - if len(diffResult.Update) > 0 { - for _, updateInput := range diffResult.Update { - updateInput.Namespace = params.Namespace - - phase, err := a.UpdatePhase(ctx, updateInput) - if err != nil { - return nil, fmt.Errorf("failed to update PlanPhase: %w", err) - } + if len(diffResult.Update) > 0 { + for _, updateInput := range diffResult.Update { + updateInput.Namespace = params.Namespace - phases = append(phases, *phase) + phase, err := a.UpdatePhase(ctx, updateInput) + if err != nil { + return nil, fmt.Errorf("failed to update PlanPhase: %w", err) } - } - if len(diffResult.Keep) > 0 { - phases = append(phases, diffResult.Keep...) + phases = append(phases, *phase) } + } - if len(phases) > 0 { - plan.SortPhases(p.Phases, plan.SortPhasesByStartAfter) - } + plan.SortPhases(p.Phases, plan.SortPhasesByStartAfter) - p.Phases = phases - } + p.Phases = phases return p, nil } @@ -476,7 +485,11 @@ var planPhaseAscOrderingByStartAfterFn = func(q *entdb.PlanPhaseQuery) { } var planPhaseEagerLoadRateCardsFn = func(q *entdb.PlanPhaseQuery) { - q.WithRatecards() + q.WithRatecards(rateCardEagerLoadFeaturesFn) +} + +var rateCardEagerLoadFeaturesFn = func(q *entdb.PlanRateCardQuery) { + q.WithFeatures() } type planPhasesDiffResult struct { @@ -493,7 +506,11 @@ type planPhasesDiffResult struct { Keep []plan.Phase } -func planPhasesDiff(requested, actual []plan.Phase) planPhasesDiffResult { +func (d planPhasesDiffResult) IsDiff() bool { + return len(d.Add) > 0 || len(d.Update) > 0 || len(d.Remove) > 0 +} + +func planPhasesDiff(requested, actual []plan.Phase) (planPhasesDiffResult, error) { result := planPhasesDiffResult{} inputsMap := make(map[string]plan.UpdatePhaseInput, len(requested)) @@ -545,10 +562,24 @@ func planPhasesDiff(requested, actual []plan.Phase) planPhasesDiffResult { if !input.Equal(phase) { result.Update = append(result.Update, input) phasesVisited[phaseKey] = struct{}{} - } else { - result.Keep = append(result.Keep, phase) + + continue + } + + diffResult, err := rateCardsDiff(lo.FromPtr(input.RateCards), phase.RateCards) + if err != nil { + return result, err + } + + if diffResult.IsDiff() { + result.Update = append(result.Update, input) phasesVisited[phaseKey] = struct{}{} + + continue } + + result.Keep = append(result.Keep, phase) + phasesVisited[phaseKey] = struct{}{} } // Collect phases to be deleted @@ -565,5 +596,5 @@ func planPhasesDiff(requested, actual []plan.Phase) planPhasesDiffResult { } } - return result + return result, nil } diff --git a/openmeter/productcatalog/plan/ratecard.go b/openmeter/productcatalog/plan/ratecard.go index b722ff8af..b790a6fd2 100644 --- a/openmeter/productcatalog/plan/ratecard.go +++ b/openmeter/productcatalog/plan/ratecard.go @@ -266,10 +266,6 @@ type RateCardMeta struct { func (r *RateCardMeta) Validate() error { var errs []error - if r.Feature != nil && r.EntitlementTemplate == nil { - errs = append(errs, errors.New("invalid EntitlementTemplate: must be provided if Feature is set")) - } - if r.EntitlementTemplate != nil { if err := r.EntitlementTemplate.Validate(); err != nil { errs = append(errs, fmt.Errorf("invalid EntitlementTemplate: %w", err)) diff --git a/openmeter/productcatalog/plan/service.go b/openmeter/productcatalog/plan/service.go index 1bfe0822a..8593303e3 100644 --- a/openmeter/productcatalog/plan/service.go +++ b/openmeter/productcatalog/plan/service.go @@ -184,11 +184,11 @@ func (i UpdatePlanInput) Equal(p Plan) bool { return false } - if lo.FromPtrOr(i.Description, "") != lo.FromPtrOr(p.Description, "") { + if i.Description != nil && lo.FromPtrOr(i.Description, "") != lo.FromPtrOr(p.Description, "") { return false } - if !MetadataEqual(lo.FromPtrOr(i.Metadata, nil), p.Metadata) { + if i.Metadata != nil && !MetadataEqual(*i.Metadata, p.Metadata) { return false } diff --git a/openmeter/productcatalog/plan/service/phase.go b/openmeter/productcatalog/plan/service/phase.go index f14354c8b..428ab0a51 100644 --- a/openmeter/productcatalog/plan/service/phase.go +++ b/openmeter/productcatalog/plan/service/phase.go @@ -51,6 +51,12 @@ func (s service) CreatePhase(ctx context.Context, params plan.CreatePhaseInput) logger.Debug("creating PlanPhase") + if len(params.RateCards) > 0 { + if err := s.expandFeatures(ctx, params.Namespace, ¶ms.RateCards); err != nil { + return nil, fmt.Errorf("failed to expand Features for RateCards in PlanPhase: %w", err) + } + } + phase, err := s.adapter.CreatePhase(ctx, params) if err != nil { return nil, fmt.Errorf("failed to create PlanPhase: %w", err) diff --git a/openmeter/productcatalog/plan/service/plan.go b/openmeter/productcatalog/plan/service/plan.go index 86fbf1e64..3a8837c57 100644 --- a/openmeter/productcatalog/plan/service/plan.go +++ b/openmeter/productcatalog/plan/service/plan.go @@ -25,48 +25,51 @@ func (s service) ListPlans(ctx context.Context, params plan.ListPlansInput) (pag return transaction.Run(ctx, s.adapter, fn) } -func (s service) expandFeatures(ctx context.Context, phases []plan.Phase) error { - if len(phases) > 0 { +func (s service) expandFeatures(ctx context.Context, namespace string, rateCards *[]plan.RateCard) error { + if rateCards == nil || len(*rateCards) == 0 { return nil } - for _, phase := range phases { - rateCardFeatures := make(map[string]*feature.Feature) - - for _, rateCard := range phase.RateCards { - if rateCardFeature := rateCard.Feature(); rateCardFeature != nil { - rateCardFeatures[rateCardFeature.Key] = rateCardFeature - } + rateCardFeatures := make(map[string]*feature.Feature) + rateCardFeatureKeys := make([]string, 0) + for _, rateCard := range *rateCards { + if rateCardFeature := rateCard.Feature(); rateCardFeature != nil { + rateCardFeatures[rateCardFeature.Key] = rateCardFeature + rateCardFeatureKeys = append(rateCardFeatureKeys, rateCardFeature.Key) } + } - featureList, err := s.feature.ListFeatures(ctx, feature.ListFeaturesParams{ - IDsOrKeys: lo.Keys(rateCardFeatures), - Namespace: phase.Namespace, - Page: pagination.Page{ - PageSize: len(rateCardFeatures), - PageNumber: 1, - }, - }) - if err != nil { - return fmt.Errorf("failed to list Features for RateCard: %w", err) - } + if len(rateCardFeatureKeys) == 0 { + return nil + } - // Update features in-place or return error if - visited := make(map[string]struct{}) - for _, feat := range featureList.Items { - if rcFeat, ok := rateCardFeatures[feat.Key]; ok { - *rcFeat = feat + featureList, err := s.feature.ListFeatures(ctx, feature.ListFeaturesParams{ + IDsOrKeys: rateCardFeatureKeys, + Namespace: namespace, + Page: pagination.Page{ + PageSize: len(rateCardFeatures), + PageNumber: 1, + }, + }) + if err != nil { + return fmt.Errorf("failed to list Features for RateCards: %w", err) + } - visited[feat.Key] = struct{}{} - } + // Update features in-place or return error if + visited := make([]string, 0) + for _, feat := range featureList.Items { + if rcFeat, ok := rateCardFeatures[feat.Key]; ok { + *rcFeat = feat + + visited = append(visited, feat.Key) } + } - if len(rateCardFeatures) != len(visited) { - missing, r := lo.Difference(lo.Keys(rateCardFeatures), lo.Keys(visited)) - missing = append(missing, r...) + if len(rateCardFeatures) != len(visited) { + missing, r := lo.Difference(rateCardFeatureKeys, visited) + missing = append(missing, r...) - return fmt.Errorf("non-existing Features: %+v", missing) - } + return fmt.Errorf("non-existing Features: %+v", missing) } return nil @@ -86,8 +89,12 @@ func (s service) CreatePlan(ctx context.Context, params plan.CreatePlanInput) (* logger.Debug("creating Plan") - if err := s.expandFeatures(ctx, params.Phases); err != nil { - return nil, fmt.Errorf("failed to get Feature for RateCards: %w", err) + if len(params.Phases) > 0 { + for _, phase := range params.Phases { + if err := s.expandFeatures(ctx, params.Namespace, &phase.RateCards); err != nil { + return nil, fmt.Errorf("failed to expand Features for RateCards in PlanPhase: %w", err) + } + } } p, err := s.adapter.CreatePlan(ctx, params) @@ -188,12 +195,13 @@ func (s service) UpdatePlan(ctx context.Context, params plan.UpdatePlanInput) (* "namespace", params.Namespace, "plan.id", params.ID, ) - logger.Debug("updating Plan") - if params.Phases != nil { - if err := s.expandFeatures(ctx, *params.Phases); err != nil { - return nil, fmt.Errorf("failed to get Feature for RateCards: %w", err) + if params.Phases != nil && len(*params.Phases) > 0 { + for _, phase := range *params.Phases { + if err := s.expandFeatures(ctx, params.Namespace, &phase.RateCards); err != nil { + return nil, fmt.Errorf("failed to expand Features for RateCards in PlanPhase: %w", err) + } } } diff --git a/openmeter/productcatalog/plan/service/service_test.go b/openmeter/productcatalog/plan/service/service_test.go index 07661b461..ce8f73930 100644 --- a/openmeter/productcatalog/plan/service/service_test.go +++ b/openmeter/productcatalog/plan/service/service_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" entdb "github.com/openmeterio/openmeter/openmeter/ent/db" + "github.com/openmeterio/openmeter/openmeter/entitlement" "github.com/openmeterio/openmeter/openmeter/meter" productcatalogadapter "github.com/openmeterio/openmeter/openmeter/productcatalog/adapter" "github.com/openmeterio/openmeter/openmeter/productcatalog/feature" @@ -95,7 +96,7 @@ func TestPlanService(t *testing.T) { assert.Equalf(t, plan.DraftStatus, getPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.DraftStatus, getPlan.Status()) }) - t.Run("New phase", func(t *testing.T) { + t.Run("NewPhase", func(t *testing.T) { updatedPhases := slices.Clone(draftPlan.Phases) updatedPhases = append(updatedPhases, plan.Phase{ NamespacedID: models.NamespacedID{ @@ -113,13 +114,26 @@ func TestPlanService(t *testing.T) { NamespacedID: models.NamespacedID{ Namespace: namespace, }, - Key: "pro-2-ratecard-1", - Type: plan.UsageBasedRateCardType, - Name: "Pro-2 RateCard 1", - Description: lo.ToPtr("Pro-2 RateCard 1"), - Metadata: map[string]string{"name": "pro-ratecard-1"}, - Feature: nil, - EntitlementTemplate: nil, + Key: "pro-2-ratecard-1", + Type: plan.UsageBasedRateCardType, + Name: "Pro-2 RateCard 1", + Description: lo.ToPtr("Pro-2 RateCard 1"), + Metadata: map[string]string{"name": "pro-2-ratecard-1"}, + Feature: &feature.Feature{ + Namespace: namespace, + Key: "api_requests_total", + }, + EntitlementTemplate: lo.ToPtr(plan.NewEntitlementTemplateFrom(plan.MeteredEntitlementTemplate{ + EntitlementTemplateMeta: plan.EntitlementTemplateMeta{ + Type: entitlement.EntitlementTypeMetered, + }, + Metadata: nil, + IsSoftLimit: true, + IssueAfterReset: lo.ToPtr(500.0), + IssueAfterResetPriority: lo.ToPtr[uint8](1), + PreserveOverageAtReset: lo.ToPtr(true), + UsagePeriod: MonthPeriod, + })), TaxConfig: &plan.TaxConfig{ Stripe: &plan.StripeTaxConfig{ Code: "txcd_10000000", @@ -166,92 +180,92 @@ func TestPlanService(t *testing.T) { assert.Equalf(t, plan.DraftStatus, updatedPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.DraftStatus, updatedPlan.Status()) plan.AssertPlanPhasesEqual(t, updatedPhases, updatedPlan.Phases) - }) - t.Run("Update phase", func(t *testing.T) { - updatedPhases := slices.Clone(draftPlan.Phases) - updatedPhases = append(updatedPhases, plan.Phase{ - NamespacedID: models.NamespacedID{ - Namespace: namespace, - }, - Key: "pro-2", - Name: "Pro-2", - Description: lo.ToPtr("Pro-2 phase"), - Metadata: map[string]string{"name": "pro-2"}, - StartAfter: SixMonthPeriod, - PlanID: draftPlan.ID, - RateCards: []plan.RateCard{ - plan.NewRateCardFrom(plan.UsageBasedRateCard{ - RateCardMeta: plan.RateCardMeta{ - NamespacedID: models.NamespacedID{ - Namespace: namespace, - }, - Key: "pro-2-ratecard-1", - Type: plan.UsageBasedRateCardType, - Name: "Pro-2 RateCard 1", - Description: lo.ToPtr("Pro-2 RateCard 1"), - Metadata: map[string]string{"name": "pro-ratecard-1"}, - Feature: nil, - EntitlementTemplate: nil, - TaxConfig: &plan.TaxConfig{ - Stripe: &plan.StripeTaxConfig{ - Code: "txcd_10000000", + t.Run("Update", func(t *testing.T) { + updatedPhases = slices.Clone(draftPlan.Phases) + updatedPhases = append(updatedPhases, plan.Phase{ + NamespacedID: models.NamespacedID{ + Namespace: namespace, + }, + Key: "pro-2", + Name: "Pro-2", + Description: lo.ToPtr("Pro-2 phase"), + Metadata: map[string]string{"name": "pro-2"}, + StartAfter: SixMonthPeriod, + PlanID: draftPlan.ID, + RateCards: []plan.RateCard{ + plan.NewRateCardFrom(plan.UsageBasedRateCard{ + RateCardMeta: plan.RateCardMeta{ + NamespacedID: models.NamespacedID{ + Namespace: namespace, }, - }, - }, - BillingCadence: MonthPeriod, - Price: lo.ToPtr(plan.NewPriceFrom(plan.TieredPrice{ - PriceMeta: plan.PriceMeta{ - Type: plan.TieredPriceType, - }, - Mode: plan.VolumeTieredPrice, - Tiers: []plan.PriceTier{ - { - UpToAmount: lo.ToPtr(decimal.NewFromInt(1000)), - FlatPrice: &plan.PriceTierFlatPrice{ - Amount: decimal.NewFromInt(50), - }, - UnitPrice: &plan.PriceTierUnitPrice{ - Amount: decimal.NewFromInt(25), + Key: "pro-2-ratecard-1", + Type: plan.UsageBasedRateCardType, + Name: "Pro-2 RateCard 1", + Description: lo.ToPtr("Pro-2 RateCard 1"), + Metadata: map[string]string{"name": "pro-ratecard-1"}, + Feature: nil, + EntitlementTemplate: nil, + TaxConfig: &plan.TaxConfig{ + Stripe: &plan.StripeTaxConfig{ + Code: "txcd_10000000", }, }, }, - MinimumAmount: lo.ToPtr(decimal.NewFromInt(1000)), - MaximumAmount: nil, - })), - }), - }, - }) + BillingCadence: MonthPeriod, + Price: lo.ToPtr(plan.NewPriceFrom(plan.TieredPrice{ + PriceMeta: plan.PriceMeta{ + Type: plan.TieredPriceType, + }, + Mode: plan.VolumeTieredPrice, + Tiers: []plan.PriceTier{ + { + UpToAmount: lo.ToPtr(decimal.NewFromInt(1000)), + FlatPrice: &plan.PriceTierFlatPrice{ + Amount: decimal.NewFromInt(50), + }, + UnitPrice: &plan.PriceTierUnitPrice{ + Amount: decimal.NewFromInt(25), + }, + }, + }, + MinimumAmount: lo.ToPtr(decimal.NewFromInt(1000)), + MaximumAmount: nil, + })), + }), + }, + }) - updateInput := plan.UpdatePlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: planInput.Namespace, - ID: draftPlan.ID, - }, - Phases: lo.ToPtr(updatedPhases), - } + updateInput = plan.UpdatePlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: planInput.Namespace, + ID: draftPlan.ID, + }, + Phases: lo.ToPtr(updatedPhases), + } - updatedPlan, err := env.Plan.UpdatePlan(ctx, updateInput) - require.NoErrorf(t, err, "updating draft Plan must not fail") - require.NotNil(t, updatedPlan, "updated draft Plan must not be empty") + updatedPlan, err = env.Plan.UpdatePlan(ctx, updateInput) + require.NoErrorf(t, err, "updating draft Plan must not fail") + require.NotNil(t, updatedPlan, "updated draft Plan must not be empty") - plan.AssertPlanPhasesEqual(t, updatedPhases, updatedPlan.Phases) - }) + plan.AssertPlanPhasesEqual(t, updatedPhases, updatedPlan.Phases) + }) - t.Run("Remove phase", func(t *testing.T) { - updateInput := plan.UpdatePlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: planInput.Namespace, - ID: draftPlan.ID, - }, - Phases: lo.ToPtr(draftPlan.Phases), - } + t.Run("Delete", func(t *testing.T) { + updateInput = plan.UpdatePlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: planInput.Namespace, + ID: draftPlan.ID, + }, + Phases: lo.ToPtr(draftPlan.Phases), + } - updatedPlan, err := env.Plan.UpdatePlan(ctx, updateInput) - require.NoErrorf(t, err, "updating draft Plan must not fail") - require.NotNil(t, updatedPlan, "updated draft Plan must not be empty") + updatedPlan, err = env.Plan.UpdatePlan(ctx, updateInput) + require.NoErrorf(t, err, "updating draft Plan must not fail") + require.NotNil(t, updatedPlan, "updated draft Plan must not be empty") - plan.AssertPlanEqual(t, *updatedPlan, *draftPlan) + plan.AssertPlanEqual(t, *updatedPlan, *draftPlan) + }) }) var publishedPlan *plan.Plan @@ -276,23 +290,23 @@ func TestPlanService(t *testing.T) { assert.Equalf(t, publishAt, *publishedPlan.EffectiveFrom, "EffectiveFrom for published Plan mismatch: expected=%s, actual=%s", publishAt, *publishedPlan.EffectiveFrom) assert.Equalf(t, plan.ActiveStatus, publishedPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.ActiveStatus, publishedPlan.Status()) - }) - t.Run("Update after publish", func(t *testing.T) { - updateInput := plan.UpdatePlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: draftPlan.Namespace, - ID: draftPlan.ID, - }, - Name: lo.ToPtr("Invalid Update"), - } + t.Run("Update", func(t *testing.T) { + updateInput := plan.UpdatePlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: draftPlan.Namespace, + ID: draftPlan.ID, + }, + Name: lo.ToPtr("Invalid Update"), + } - _, err = env.Plan.UpdatePlan(ctx, updateInput) - require.Errorf(t, err, "updating active Plan must fail") + _, err = env.Plan.UpdatePlan(ctx, updateInput) + require.Errorf(t, err, "updating active Plan must fail") + }) }) var nextPlan *plan.Plan - t.Run("New version", func(t *testing.T) { + t.Run("NewVersion", func(t *testing.T) { nextInput := plan.NextPlanInput{ NamespacedID: models.NamespacedID{ Namespace: publishedPlan.Namespace, @@ -306,87 +320,87 @@ func TestPlanService(t *testing.T) { assert.Equalf(t, publishedPlan.Version+1, nextPlan.Version, "new draft Plan must have higher version number") assert.Equalf(t, plan.DraftStatus, nextPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.DraftStatus, nextPlan.Status()) - }) - var publishedNextPlan *plan.Plan - t.Run("Publish next", func(t *testing.T) { - publishAt := time.Now().Truncate(time.Microsecond) + var publishedNextPlan *plan.Plan + t.Run("Publish", func(t *testing.T) { + publishAt := time.Now().Truncate(time.Microsecond) - publishInput := plan.PublishPlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: nextPlan.Namespace, - ID: nextPlan.ID, - }, - EffectivePeriod: plan.EffectivePeriod{ - EffectiveFrom: &publishAt, - EffectiveTo: nil, - }, - } + publishInput := plan.PublishPlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: nextPlan.Namespace, + ID: nextPlan.ID, + }, + EffectivePeriod: plan.EffectivePeriod{ + EffectiveFrom: &publishAt, + EffectiveTo: nil, + }, + } - publishedNextPlan, err = env.Plan.PublishPlan(ctx, publishInput) - require.NoErrorf(t, err, "publishing draft Plan must not fail") - require.NotNil(t, publishedNextPlan, "published Plan must not be empty") - require.NotNil(t, publishedNextPlan.EffectiveFrom, "EffectiveFrom for published Plan must not be empty") + publishedNextPlan, err = env.Plan.PublishPlan(ctx, publishInput) + require.NoErrorf(t, err, "publishing draft Plan must not fail") + require.NotNil(t, publishedNextPlan, "published Plan must not be empty") + require.NotNil(t, publishedNextPlan.EffectiveFrom, "EffectiveFrom for published Plan must not be empty") - assert.Equalf(t, publishAt, *publishedNextPlan.EffectiveFrom, "EffectiveFrom for published Plan mismatch: expected=%s, actual=%s", publishAt, *publishedPlan.EffectiveFrom) - assert.Equalf(t, plan.ActiveStatus, publishedNextPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.ActiveStatus, publishedNextPlan.Status()) + assert.Equalf(t, publishAt, *publishedNextPlan.EffectiveFrom, "EffectiveFrom for published Plan mismatch: expected=%s, actual=%s", publishAt, *publishedPlan.EffectiveFrom) + assert.Equalf(t, plan.ActiveStatus, publishedNextPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.ActiveStatus, publishedNextPlan.Status()) - prevPlan, err := env.Plan.GetPlan(ctx, plan.GetPlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: publishedPlan.Namespace, - ID: publishedPlan.ID, - }, + prevPlan, err := env.Plan.GetPlan(ctx, plan.GetPlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: publishedPlan.Namespace, + ID: publishedPlan.ID, + }, + }) + require.NoErrorf(t, err, "getting previous Plan version must not fail") + require.NotNil(t, prevPlan, "previous Plan version must not be empty") + + assert.Equalf(t, plan.ArchivedStatus, prevPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.ArchivedStatus, prevPlan.Status()) }) - require.NoErrorf(t, err, "getting previous Plan version must not fail") - require.NotNil(t, prevPlan, "previous Plan version must not be empty") - assert.Equalf(t, plan.ArchivedStatus, prevPlan.Status(), "Plan Status mismatch: expected=%s, actual=%s", plan.ArchivedStatus, prevPlan.Status()) - }) + var archivedPlan *plan.Plan + t.Run("Archive", func(t *testing.T) { + archiveAt := time.Now().Truncate(time.Microsecond) - var archivedPlan *plan.Plan - t.Run("Archive next", func(t *testing.T) { - archiveAt := time.Now().Truncate(time.Microsecond) + archiveInput := plan.ArchivePlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: nextPlan.Namespace, + ID: nextPlan.ID, + }, + EffectiveTo: archiveAt, + } - archiveInput := plan.ArchivePlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: nextPlan.Namespace, - ID: nextPlan.ID, - }, - EffectiveTo: archiveAt, - } + archivedPlan, err = env.Plan.ArchivePlan(ctx, archiveInput) + require.NoErrorf(t, err, "archiving Plan must not fail") + require.NotNil(t, archivedPlan, "archived Plan must not be empty") + require.NotNil(t, archivedPlan.EffectiveTo, "EffectiveFrom for archived Plan must not be empty") - archivedPlan, err = env.Plan.ArchivePlan(ctx, archiveInput) - require.NoErrorf(t, err, "archiving Plan must not fail") - require.NotNil(t, archivedPlan, "archived Plan must not be empty") - require.NotNil(t, archivedPlan.EffectiveTo, "EffectiveFrom for archived Plan must not be empty") + assert.Equalf(t, archiveAt, *archivedPlan.EffectiveTo, "EffectiveTo for published Plan mismatch: expected=%s, actual=%s", archiveAt, *archivedPlan.EffectiveTo) + assert.Equalf(t, plan.ArchivedStatus, archivedPlan.Status(), "Status mismatch for archived Plan: expected=%s, actual=%s", plan.ArchivedStatus, archivedPlan.Status()) + }) - assert.Equalf(t, archiveAt, *archivedPlan.EffectiveTo, "EffectiveTo for published Plan mismatch: expected=%s, actual=%s", archiveAt, *archivedPlan.EffectiveTo) - assert.Equalf(t, plan.ArchivedStatus, archivedPlan.Status(), "Status mismatch for archived Plan: expected=%s, actual=%s", plan.ArchivedStatus, archivedPlan.Status()) - }) + t.Run("Delete", func(t *testing.T) { + deleteInput := plan.DeletePlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: archivedPlan.Namespace, + ID: archivedPlan.ID, + }, + } - t.Run("Delete next", func(t *testing.T) { - deleteInput := plan.DeletePlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: archivedPlan.Namespace, - ID: archivedPlan.ID, - }, - } + err = env.Plan.DeletePlan(ctx, deleteInput) + require.NoErrorf(t, err, "deleting Plan must not fail") + require.NotNil(t, archivedPlan, "archived Plan must not be empty") + require.NotNil(t, archivedPlan.EffectiveTo, "EffectiveFrom for archived Plan must not be empty") - err = env.Plan.DeletePlan(ctx, deleteInput) - require.NoErrorf(t, err, "deleting Plan must not fail") - require.NotNil(t, archivedPlan, "archived Plan must not be empty") - require.NotNil(t, archivedPlan.EffectiveTo, "EffectiveFrom for archived Plan must not be empty") + deletedPlan, err := env.Plan.GetPlan(ctx, plan.GetPlanInput{ + NamespacedID: models.NamespacedID{ + Namespace: archivedPlan.Namespace, + ID: archivedPlan.ID, + }, + }) + require.NoErrorf(t, err, "getting deleted Plan version must not fail") + require.NotNil(t, deletedPlan, "deleted Plan version must not be empty") - deletedPlan, err := env.Plan.GetPlan(ctx, plan.GetPlanInput{ - NamespacedID: models.NamespacedID{ - Namespace: archivedPlan.Namespace, - ID: archivedPlan.ID, - }, + assert.NotNilf(t, deletedPlan.DeletedAt, "deletedAt must not be empty") }) - require.NoErrorf(t, err, "getting deleted Plan version must not fail") - require.NotNil(t, deletedPlan, "deleted Plan version must not be empty") - - assert.NotNilf(t, deletedPlan.DeletedAt, "deletedAt must not be empty") }) }) })