| package subscription_test |
|
|
| import ( |
| "errors" |
| "testing" |
| "time" |
|
|
| decimal "github.com/alpacahq/alpacadecimal" |
| "github.com/samber/lo" |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
|
|
| "github.com/openmeterio/openmeter/openmeter/productcatalog" |
| "github.com/openmeterio/openmeter/openmeter/subscription" |
| "github.com/openmeterio/openmeter/pkg/clock" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| func TestGetFullServicePeriodAtInputValidate(t *testing.T) { |
| clock.FreezeTime(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) |
| t.Cleanup(clock.UnFreeze) |
|
|
| tests := []struct { |
| name string |
| inp subscription.GetFullServicePeriodAtInput |
| want error |
| }{ |
| { |
| name: "missing at", |
| inp: subscription.GetFullServicePeriodAtInput{}, |
| want: errors.New("at is zero"), |
| }, |
| { |
| name: "missing aligned billing anchor", |
| inp: subscription.GetFullServicePeriodAtInput{ |
| At: clock.Now(), |
| }, |
| want: errors.New("aligned billing anchor is zero"), |
| }, |
| { |
| name: "at outside of subscription period", |
| inp: subscription.GetFullServicePeriodAtInput{ |
| At: clock.Now().Add(time.Hour), |
| AlignedBillingAnchor: clock.Now(), |
| SubscriptionCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now()), |
| }, |
| }, |
| want: errors.New("subscription is not active at 2020-01-01 01:00:00 +0000 UTC: [2019-12-31 23:00:00 +0000 UTC, 2020-01-01 00:00:00 +0000 UTC]"), |
| }, |
| { |
| name: "at outside of phase cadence", |
| inp: subscription.GetFullServicePeriodAtInput{ |
| At: clock.Now().Add(time.Minute), |
| AlignedBillingAnchor: clock.Now(), |
| SubscriptionCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now().Add(time.Hour)), |
| }, |
| PhaseCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now()), |
| }, |
| }, |
| want: errors.New("phase is not active at 2020-01-01 00:01:00 +0000 UTC: [2019-12-31 23:00:00 +0000 UTC, 2020-01-01 00:00:00 +0000 UTC]"), |
| }, |
| { |
| name: "at outside of item cadence", |
| inp: subscription.GetFullServicePeriodAtInput{ |
| At: clock.Now().Add(time.Minute), |
| AlignedBillingAnchor: clock.Now(), |
| SubscriptionCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now().Add(time.Hour)), |
| }, |
| PhaseCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now().Add(time.Hour)), |
| }, |
| ItemCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now()), |
| }, |
| }, |
| want: errors.New("item is not active at 2020-01-01 00:01:00 +0000 UTC: [2019-12-31 23:00:00 +0000 UTC, 2020-01-01 00:00:00 +0000 UTC]"), |
| }, |
| { |
| name: "for a zero length item during a phase", |
| inp: subscription.GetFullServicePeriodAtInput{ |
| At: clock.Now(), |
| AlignedBillingAnchor: clock.Now(), |
| SubscriptionCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now().Add(time.Hour)), |
| }, |
| PhaseCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now().Add(time.Hour)), |
| }, |
| ItemCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now(), |
| ActiveTo: lo.ToPtr(clock.Now()), |
| }, |
| }, |
| want: nil, |
| }, |
| { |
| name: "at end of subscription for a zero length last item", |
| inp: subscription.GetFullServicePeriodAtInput{ |
| At: clock.Now(), |
| AlignedBillingAnchor: clock.Now(), |
| SubscriptionCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now()), |
| }, |
| PhaseCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now().Add(-time.Hour), |
| ActiveTo: lo.ToPtr(clock.Now()), |
| }, |
| ItemCadence: models.CadencedModel{ |
| ActiveFrom: clock.Now(), |
| ActiveTo: lo.ToPtr(clock.Now()), |
| }, |
| }, |
| want: nil, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if tt.want == nil { |
| require.NoError(t, tt.inp.Validate()) |
| } else { |
| require.ErrorContains(t, tt.inp.Validate(), tt.want.Error()) |
| } |
| }) |
| } |
| } |
|
|
| func TestSubscriptionSpecHasUnitConfig(t *testing.T) { |
| card := func(uc *productcatalog.UnitConfig) productcatalog.RateCard { |
| return &productcatalog.UsageBasedRateCard{ |
| RateCardMeta: productcatalog.RateCardMeta{ |
| Key: "feat-1", |
| Name: "Feature 1", |
| FeatureKey: lo.ToPtr("feat-1"), |
| Price: productcatalog.NewPriceFrom(productcatalog.UnitPrice{Amount: decimal.NewFromInt(1)}), |
| UnitConfig: uc, |
| }, |
| } |
| } |
| item := func(uc *productcatalog.UnitConfig) *subscription.SubscriptionItemSpec { |
| return &subscription.SubscriptionItemSpec{ |
| CreateSubscriptionItemInput: subscription.CreateSubscriptionItemInput{ |
| CreateSubscriptionItemPlanInput: subscription.CreateSubscriptionItemPlanInput{ |
| PhaseKey: "phase-1", |
| ItemKey: "item-1", |
| RateCard: card(uc), |
| }, |
| }, |
| } |
| } |
| specWith := func(items ...*subscription.SubscriptionItemSpec) subscription.SubscriptionSpec { |
| return subscription.SubscriptionSpec{ |
| Phases: map[string]*subscription.SubscriptionPhaseSpec{ |
| "phase-1": { |
| ItemsByKey: map[string][]*subscription.SubscriptionItemSpec{"item-1": items}, |
| }, |
| }, |
| } |
| } |
| divide := &productcatalog.UnitConfig{Operation: productcatalog.UnitConfigOperationDivide, ConversionFactor: decimal.NewFromInt(1000)} |
|
|
| t.Run("empty spec has none", func(t *testing.T) { |
| s := subscription.SubscriptionSpec{} |
| assert.False(t, s.HasUnitConfig()) |
| }) |
|
|
| t.Run("no item carries unit_config", func(t *testing.T) { |
| s := specWith(item(nil)) |
| assert.False(t, s.HasUnitConfig()) |
| }) |
|
|
| t.Run("an item carrying unit_config is detected", func(t *testing.T) { |
| s := specWith(item(nil), item(divide)) |
| assert.True(t, s.HasUnitConfig()) |
| }) |
| } |
|
|