| package subscriptiontestutils |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| "testing" |
| "time" |
|
|
| "github.com/samber/lo" |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
|
|
| "github.com/openmeterio/openmeter/openmeter/entitlement" |
| "github.com/openmeterio/openmeter/openmeter/subscription" |
| subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon" |
| "github.com/openmeterio/openmeter/pkg/datetime" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| |
| func ValidateSpecAndView(t *testing.T, expected subscription.SubscriptionSpec, found subscription.SubscriptionView) { |
| t.Helper() |
|
|
| |
| assert.Equal(t, expected.Name, found.Subscription.Name) |
| assert.Equal(t, expected.Description, found.Subscription.Description) |
| assert.Equal(t, expected.Plan, found.Subscription.PlanRef) |
| assert.Equal(t, expected.Currency, found.Subscription.Currency) |
| assert.Equal(t, expected.CustomerId, found.Subscription.CustomerId) |
| assert.Equal(t, expected.ActiveFrom, found.Subscription.ActiveFrom) |
| assert.Equal(t, expected.ActiveTo, found.Subscription.ActiveTo) |
| assert.Equal(t, expected.Metadata, found.Subscription.Metadata) |
| assert.Equal(t, expected.BillingAnchor, found.Subscription.BillingAnchor) |
|
|
| |
|
|
| foundPhases := found.Phases |
| specPhases := expected.GetSortedPhases() |
|
|
| require.Equal(t, len(specPhases), len(foundPhases), "phase count mismatch") |
|
|
| for i := range specPhases { |
| specPhase := specPhases[i] |
| foundPhase := foundPhases[i] |
|
|
| |
| assert.Equal(t, specPhase.PhaseKey, foundPhase.SubscriptionPhase.Key) |
| assert.Equal(t, specPhase.Name, foundPhase.SubscriptionPhase.Name) |
| assert.Equal(t, specPhase.Description, foundPhase.SubscriptionPhase.Description) |
| assert.Equal(t, specPhase.Metadata, foundPhase.SubscriptionPhase.Metadata) |
|
|
| expectedStart, _ := specPhases[i].StartAfter.AddTo(found.Subscription.ActiveFrom) |
| assert.Equal(t, expectedStart.UTC(), foundPhases[i].SubscriptionPhase.ActiveFrom.UTC()) |
|
|
| |
| specItemsByKey := specPhase.ItemsByKey |
| foundItemsByKey := foundPhase.ItemsByKey |
|
|
| require.Equal(t, len(specItemsByKey), len(foundItemsByKey), "item count mismatch for phase %s", specPhase.PhaseKey) |
|
|
| for specItemsKey := range specItemsByKey { |
| specItemsByKey, ok := specItemsByKey[specItemsKey] |
| require.True(t, ok, "item %s not found in spec phase %s", specItemsKey, specPhase.PhaseKey) |
| foundItemsByKey, ok := foundItemsByKey[specItemsKey] |
| require.True(t, ok, "item %s not found in found phase %s", specItemsKey, specPhase.PhaseKey) |
|
|
| require.Equal(t, len(specItemsByKey), len(foundItemsByKey), "item count mismatch for item %s in phase %s", specItemsKey, specPhase.PhaseKey) |
|
|
| for idx, specItem := range specItemsByKey { |
| foundItem := foundItemsByKey[idx] |
|
|
| |
|
|
| assert.Equal(t, specItem.ItemKey, foundItem.SubscriptionItem.Key) |
| |
| assert.Equal(t, foundPhase.SubscriptionPhase.Key, specItem.PhaseKey) |
| assert.Equal(t, foundPhase.SubscriptionPhase.ID, foundItem.SubscriptionItem.PhaseId) |
|
|
| |
| rc1JSON, _ := json.Marshal(specItem.RateCard) |
| rc2JSON, _ := json.Marshal(foundItem.SubscriptionItem.RateCard) |
|
|
| assert.True(t, specItem.RateCard.Equal(foundItem.SubscriptionItem.RateCard), "rate card mismatch for item %s in phase %s: \nspec: %s \n\nview: %s", specItem.ItemKey, specPhase.PhaseKey, rc1JSON, rc2JSON) |
|
|
| |
| specMeta := specItem.RateCard.AsMeta() |
| foundMeta := foundItem.SubscriptionItem.RateCard.AsMeta() |
| if specMeta.TaxConfig != nil && specMeta.TaxConfig.TaxCodeID != nil && *specMeta.TaxConfig.TaxCodeID != "" { |
| require.NotNil(t, foundMeta.TaxConfig, "TaxConfig must be set for item %s (phase %s) with TaxCodeID %s", specItem.ItemKey, specPhase.PhaseKey, *specMeta.TaxConfig.TaxCodeID) |
| assert.Equal(t, specMeta.TaxConfig.TaxCodeID, foundMeta.TaxConfig.TaxCodeID, "TaxCodeID mismatch for item %s in phase %s", specItem.ItemKey, specPhase.PhaseKey) |
| } else if specMeta.TaxConfig != nil && specMeta.TaxConfig.Stripe != nil && specMeta.TaxConfig.Stripe.Code != "" { |
| require.NotNil(t, foundMeta.TaxConfig, "TaxConfig must be set for item %s (phase %s) with Stripe tax code %s", specItem.ItemKey, specPhase.PhaseKey, specMeta.TaxConfig.Stripe.Code) |
| require.NotNil(t, foundMeta.TaxConfig.TaxCodeID, "TaxCodeID must be backfilled for item %s (phase %s) with Stripe tax code %s", specItem.ItemKey, specPhase.PhaseKey, specMeta.TaxConfig.Stripe.Code) |
| assert.Equal(t, specMeta.TaxConfig.TaxCodeID, foundMeta.TaxConfig.TaxCodeID, "TaxCodeID mismatch for item %s in phase %s", specItem.ItemKey, specPhase.PhaseKey) |
| } else if foundMeta.TaxConfig != nil { |
| assert.Nil(t, foundMeta.TaxConfig.TaxCodeID, "TaxCodeID must be nil for item %s (phase %s) without a tax code", specItem.ItemKey, specPhase.PhaseKey) |
| } |
|
|
| |
| pFeatureKey := specItem.RateCard.AsMeta().FeatureKey |
| if foundItem.SubscriptionItem.RateCard.AsMeta().FeatureKey != nil { |
| require.NotNil(t, pFeatureKey) |
| assert.Equal(t, pFeatureKey, foundItem.SubscriptionItem.RateCard.AsMeta().FeatureKey) |
| } else { |
| assert.Empty(t, pFeatureKey) |
| } |
|
|
| rcInp := specItem.CreateSubscriptionItemPlanInput |
|
|
| |
| if rcEnt := rcInp.RateCard.AsMeta().EntitlementTemplate; rcEnt != nil { |
| ent := foundItem.Entitlement |
| exists := ent != nil |
| require.True(t, exists) |
| entInp := ent.ToScheduleSubscriptionEntitlementInput() |
| assert.Equal(t, rcEnt.Type(), entInp.CreateEntitlementInputs.GetType()) |
|
|
| |
| assert.Equal(t, foundItem.Entitlement.Entitlement.Annotations[subscription.AnnotationSubscriptionID], found.Subscription.NamespacedID.ID) |
|
|
| |
| period := GetEntitlementTemplateUsagePeriod(t, *specItem.RateCard.AsMeta().EntitlementTemplate) |
| require.NotNil(t, period) |
|
|
| |
| truncatedBillingAnchor := found.Subscription.BillingAnchor.Truncate(time.Minute) |
| |
| entPeriod := ent.Entitlement.UsagePeriod.GetOriginalValueAsUsagePeriodInput().GetValue().Interval.ISODuration |
| assert.True(t, entPeriod.Equal(period), "usage period interval mismatch, expected %s, got %s", period, entPeriod) |
| |
| require.NotNil(t, ent.Entitlement.UsagePeriod) |
|
|
| |
| |
| recAtAnchor, _, err := ent.Entitlement.UsagePeriod.GetUsagePeriodInputAt(truncatedBillingAnchor) |
| require.NoError(t, err) |
|
|
| entPerAtAnchor, err := recAtAnchor.GetValue().GetPeriodAt(truncatedBillingAnchor) |
| require.NoError(t, err) |
|
|
| require.Equal(t, truncatedBillingAnchor, entPerAtAnchor.From, "entitlement usage period anchor should be aligned with the subscription billing anchor, subscription billing anchor: %s, entitlement usage period: %+v", truncatedBillingAnchor, *ent.Entitlement.UsagePeriod) |
|
|
| switch rcInp.RateCard.AsMeta().EntitlementTemplate.Type() { |
| case entitlement.EntitlementTypeMetered: |
| |
| require.NotNil(t, ent.Entitlement.MeasureUsageFrom) |
| assert.Equal(t, foundPhase.SubscriptionPhase.ActiveFrom.UTC().Truncate(time.Minute), ent.Entitlement.MeasureUsageFrom.UTC().Truncate(time.Minute), "measureUsageFrom should equal the truncated phase start, expected %s, got %s", foundPhase.SubscriptionPhase.ActiveFrom.UTC().Truncate(time.Minute), ent.Entitlement.MeasureUsageFrom.UTC().Truncate(time.Minute)) |
| } |
|
|
| |
| require.NotNil(t, ent.Entitlement.ActiveFrom) |
| assert.Equal(t, foundItem.SubscriptionItem.ActiveFrom, *ent.Entitlement.ActiveFrom) |
|
|
| |
| assert.Equal(t, foundItem.SubscriptionItem.ActiveTo, ent.Entitlement.ActiveTo) |
| } else { |
| |
| assert.Nil(t, foundItem.Entitlement) |
| } |
| } |
| } |
| } |
| } |
|
|
| func SpecsEqual(t *testing.T, s1, s2 subscription.SubscriptionSpec) { |
| t.Helper() |
|
|
| |
| assert.Equal(t, s1.Name, s2.Name) |
| assert.Equal(t, s1.Description, s2.Description) |
| assert.Equal(t, s1.Plan, s2.Plan) |
| assert.Equal(t, s1.Currency, s2.Currency) |
| assert.Equal(t, s1.CustomerId, s2.CustomerId) |
| assert.Equal(t, s1.ActiveFrom, s2.ActiveFrom) |
| assert.Equal(t, s1.ActiveTo, s2.ActiveTo) |
| assert.Equal(t, s1.Metadata, s2.Metadata) |
|
|
| |
| require.Equal(t, len(s1.Phases), len(s2.Phases), "phase count mismatch") |
|
|
| for key := range s1.Phases { |
| p1 := s1.Phases[key] |
| p1Cad, err := s1.GetPhaseCadence(key) |
| require.NoError(t, err) |
|
|
| p2, ok := s2.Phases[key] |
| p2Cad, err := s2.GetPhaseCadence(key) |
| require.NoError(t, err) |
|
|
| require.True(t, ok, "phase %s not found in second spec", key) |
|
|
| |
| assert.Equal(t, p1.Name, p2.Name, "mismatch for phase %s", key) |
| assert.Equal(t, p1.Description, p2.Description, "mismatch for phase %s", key) |
| assert.Equal(t, p1.Metadata, p2.Metadata, "mismatch for phase %s", key) |
| assert.Equal(t, p1.PhaseKey, p2.PhaseKey, "mismatch for phase %s", key) |
| assert.Equal(t, p1.StartAfter, p2.StartAfter, "mismatch for phase %s", key) |
|
|
| |
| require.Equal(t, len(p1.ItemsByKey), len(p2.ItemsByKey), "item count mismatch for phase %s, expected %+v and got %+v", key, lo.Keys(p1.ItemsByKey), lo.Keys(p2.ItemsByKey)) |
|
|
| for itemKey := range p1.ItemsByKey { |
| p1Items := p1.ItemsByKey[itemKey] |
| p2Items, ok := p2.ItemsByKey[itemKey] |
| require.True(t, ok, "item %s not found in phase %s", itemKey, key) |
|
|
| require.Equal( |
| t, |
| len(p1Items), |
| len(p2Items), |
| "item count mismatch for item %s in phase %s\n\nexpected: %+v\n\nfound: %+v", |
| itemKey, |
| key, |
| lo.Map(p1Items, func(item *subscription.SubscriptionItemSpec, _ int) models.CadencedModel { |
| return item.GetCadence(p1Cad) |
| }), |
| lo.Map(p2Items, func(item *subscription.SubscriptionItemSpec, _ int) models.CadencedModel { |
| return item.GetCadence(p2Cad) |
| }), |
| ) |
|
|
| for i := range p1Items { |
| i1 := p1Items[i] |
| i2 := p2Items[i] |
|
|
| |
| assert.Equal(t, i1.ItemKey, i2.ItemKey) |
| assert.True(t, i1.RateCard.Equal(i2.RateCard), "rate card mismatch for item %s in phase %s: \nspec: %+v\n\nview: %+v", itemKey, key, i1.RateCard, i2.RateCard) |
| assert.Equal(t, i1.CreateSubscriptionItemPlanInput, i2.CreateSubscriptionItemPlanInput, "create subscription item plan input mismatch for item %s in phase %s", itemKey, key) |
|
|
| |
| i1af := i1.ActiveFromOverrideRelativeToPhaseStart |
| i2af := i2.ActiveFromOverrideRelativeToPhaseStart |
|
|
| equalNilableTime( |
| t, |
| tsPlusNillableISO(p1Cad.ActiveFrom, i1af), |
| tsPlusNillableISO(p2Cad.ActiveFrom, i2af), |
| "active from override relative to phase start mismatch for item %s in phase %s", |
| itemKey, |
| key, |
| ) |
|
|
| i1at := i1.ActiveToOverrideRelativeToPhaseStart |
| i2at := i2.ActiveToOverrideRelativeToPhaseStart |
|
|
| equalNilableTime( |
| t, |
| tsPlusNillableISO(p1Cad.ActiveFrom, i1at), |
| tsPlusNillableISO(p2Cad.ActiveFrom, i2at), |
| "active to override relative to phase start mismatch for item %s in phase %s", |
| itemKey, |
| key, |
| ) |
|
|
| |
| c1 := subscription.CreateSubscriptionItemCustomerInput{ |
| BillingBehaviorOverride: i1.CreateSubscriptionItemCustomerInput.BillingBehaviorOverride, |
| } |
|
|
| c2 := subscription.CreateSubscriptionItemCustomerInput{ |
| BillingBehaviorOverride: i2.CreateSubscriptionItemCustomerInput.BillingBehaviorOverride, |
| } |
|
|
| assert.Equal(t, c1, c2, "create subscription item customer input mismatch for item %s in phase %s", itemKey, key) |
| } |
| } |
| } |
| } |
|
|
| func tsPlusNillableISO(ts time.Time, iso *datetime.ISODuration) *time.Time { |
| if iso == nil { |
| return nil |
| } |
|
|
| out, _ := iso.AddTo(ts) |
|
|
| return &out |
| } |
|
|
| func equalNilableTime(t *testing.T, t1, t2 *time.Time, msgAndArgs ...interface{}) { |
| getTpl := func() error { |
| if len(msgAndArgs) == 0 { |
| return nil |
| } |
|
|
| msg, ok := msgAndArgs[0].(string) |
| if !ok { |
| return fmt.Errorf("expected string message, got %T", msgAndArgs[0]) |
| } |
|
|
| return fmt.Errorf(msg, msgAndArgs[1:]...) |
| } |
|
|
| if t1 == nil != (t2 == nil) { |
| t.Fatalf("%s: mismatch for time %v and %v", getTpl(), t1, t2) |
| } |
|
|
| if t1 != nil { |
| assert.Equal(t, *t1, *t2, "%s: mismatch for time %v and %v", getTpl(), t1, t2) |
| } |
| } |
|
|
| func SubscriptionAddonsEqual(t *testing.T, a1, a2 subscriptionaddon.SubscriptionAddon) { |
| t.Helper() |
|
|
| assert.Equal(t, a1.Addon.ID, a2.Addon.ID) |
| assert.Equal(t, a1.SubscriptionID, a2.SubscriptionID) |
| assert.Equal(t, a1.Metadata, a2.Metadata) |
| assert.Equal(t, a1.RateCards, a2.RateCards) |
|
|
| require.Equal(t, len(a1.Quantities.GetTimes()), len(a2.Quantities.GetTimes())) |
| for i := 0; i < len(a1.Quantities.GetTimes()); i++ { |
| require.Equal(t, a1.Quantities.GetAt(i).GetValue().Quantity, a2.Quantities.GetAt(i).GetValue().Quantity) |
| require.Equal(t, a1.Quantities.GetAt(i).GetTime(), a2.Quantities.GetAt(i).GetTime()) |
| } |
| } |
|
|