| package addondiff_test |
|
|
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "testing" |
| "time" |
|
|
| "github.com/alpacahq/alpacadecimal" |
| "github.com/samber/lo" |
| "github.com/stretchr/testify/require" |
|
|
| "github.com/openmeterio/openmeter/openmeter/app" |
| "github.com/openmeterio/openmeter/openmeter/productcatalog" |
| "github.com/openmeterio/openmeter/openmeter/subscription" |
| subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon" |
| addondiff "github.com/openmeterio/openmeter/openmeter/subscription/addon/diff" |
| subscriptiontestutils "github.com/openmeterio/openmeter/openmeter/subscription/testutils" |
| "github.com/openmeterio/openmeter/openmeter/taxcode" |
| "github.com/openmeterio/openmeter/pkg/clock" |
| "github.com/openmeterio/openmeter/pkg/datetime" |
| "github.com/openmeterio/openmeter/pkg/models" |
| "github.com/openmeterio/openmeter/pkg/timeutil" |
| ) |
|
|
| var now, _ = time.Parse(time.RFC3339, "2025-04-01T00:00:00Z") |
|
|
| func TestApply(t *testing.T) { |
| type tcDeps struct { |
| deps subscriptiontestutils.SubscriptionDependencies |
| } |
|
|
| |
| runWithDeps := func(t *testing.T, fn func(t *testing.T, deps *tcDeps)) { |
| clock.FreezeTime(now.Add(time.Millisecond)) |
| defer func() { |
| clock.UnFreeze() |
| clock.ResetTime() |
| }() |
|
|
| dbDeps := subscriptiontestutils.SetupDBDeps(t) |
| defer dbDeps.Cleanup(t) |
|
|
| deps := subscriptiontestutils.NewService(t, dbDeps) |
|
|
| fn(t, &tcDeps{ |
| deps: deps, |
| }) |
| } |
|
|
| t.Run("Should return nil when there's no quantities listed", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.GetExamplePlanInput(t), |
| subscriptiontestutils.GetExampleAddonInput(t, productcatalog.EffectivePeriod{ |
| EffectiveFrom: &now, |
| EffectiveTo: nil, |
| }), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, models.CadencedModel{ |
| ActiveFrom: now, |
| ActiveTo: nil, |
| }) |
|
|
| |
| subsAdd.Quantities = timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{}) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| require.Nil(t, diffable) |
| }) |
| }) |
|
|
| t.Run("Should add the new item to the subscription", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.GetExamplePlanInput(t), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| productcatalog.EffectivePeriod{ |
| EffectiveFrom: &now, |
| EffectiveTo: nil, |
| }, |
| productcatalog.AddonInstanceTypeSingle, |
| subscriptiontestutils.ExampleAddonRateCard2.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, models.CadencedModel{ |
| ActiveFrom: now, |
| ActiveTo: nil, |
| }) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| for _, p := range spec.GetSortedPhases() { |
| b, _ := json.MarshalIndent(p, "", " ") |
|
|
| itemHistory, ok := p.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard2.Key()] |
| require.True(t, ok, "item history missing in phase %s, got %+v, \nfull: %s", p.PhaseKey, lo.Keys(p.ItemsByKey), string(b)) |
|
|
| |
| require.Len(t, itemHistory, 1) |
| item := itemHistory[0] |
|
|
| |
| pCad, _ := spec.GetPhaseCadence(p.PhaseKey) |
| cad := item.GetCadence(pCad) |
| require.True(t, cad.Equal(pCad)) |
|
|
| |
| require.Nil(t, item.ActiveFromOverrideRelativeToPhaseStart) |
| require.Nil(t, item.ActiveToOverrideRelativeToPhaseStart) |
|
|
| |
| require.Empty(t, subscription.AnnotationParser.ListOwnerSubSystems(item.Annotations)) |
|
|
| |
| require.True(t, subscriptiontestutils.ExampleAddonRateCard2.Equal(item.RateCard)) |
| } |
| }) |
| }) |
|
|
| t.Run("Should add multiple instances of new item to the subscription", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| exampleAddonRateCard3, ok := (subscriptiontestutils.ExampleAddonRateCard3.Clone()).(*productcatalog.FlatFeeRateCard) |
| require.True(t, ok, "expected FlatFeeRateCard, got %T", exampleAddonRateCard3) |
|
|
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.GetExamplePlanInput(t), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| productcatalog.EffectivePeriod{ |
| EffectiveFrom: &now, |
| EffectiveTo: nil, |
| }, |
| productcatalog.AddonInstanceTypeSingle, |
| exampleAddonRateCard3, |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, models.CadencedModel{ |
| ActiveFrom: now, |
| ActiveTo: nil, |
| }) |
|
|
| |
| val := subsAdd.Quantities.GetAt(0).GetValue() |
|
|
| val.Quantity = 3 |
|
|
| subsAdd.Quantities = timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{ |
| val.AsTimed(), |
| }) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| for _, p := range spec.GetSortedPhases() { |
| b, _ := json.MarshalIndent(p, "", " ") |
|
|
| itemHistory, ok := p.ItemsByKey[exampleAddonRateCard3.Key()] |
| require.True(t, ok, "item history missing in phase %s, got %+v, \nfull: %s", p.PhaseKey, lo.Keys(p.ItemsByKey), string(b)) |
|
|
| |
| require.Len(t, itemHistory, 1) |
| item := itemHistory[0] |
|
|
| |
| pCad, _ := spec.GetPhaseCadence(p.PhaseKey) |
| cad := item.GetCadence(pCad) |
| require.True(t, cad.Equal(pCad)) |
|
|
| |
| require.Nil(t, item.ActiveFromOverrideRelativeToPhaseStart) |
| require.Nil(t, item.ActiveToOverrideRelativeToPhaseStart) |
|
|
| |
| require.Empty(t, subscription.AnnotationParser.ListOwnerSubSystems(item.Annotations)) |
|
|
| pr, _ := item.RateCard.AsMeta().Price.AsFlat() |
| require.Equal(t, int64(300), pr.Amount.IntPart()) |
|
|
| |
| compareRateCardsWithAmountChange(t, exampleAddonRateCard3, 100*3, item.RateCard, "phase %s", p.PhaseKey) |
| } |
| }) |
| }) |
|
|
| t.Run("Should add item for defined cadence of the addon", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| effPer := productcatalog.EffectivePeriod{ |
| EffectiveFrom: lo.ToPtr(now), |
| } |
|
|
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.GetExamplePlanInput(t), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| effPer, |
| productcatalog.AddonInstanceTypeSingle, |
| subscriptiontestutils.ExampleAddonRateCard3.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| subAddCadence := models.CadencedModel{ |
| ActiveFrom: now.AddDate(0, 0, 3), |
| ActiveTo: lo.ToPtr(now.AddDate(0, 1, 8)), |
| } |
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, subAddCadence) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| for pIdx, p := range spec.GetSortedPhases() { |
| if pIdx >= 2 { |
| require.NotContains(t, p.ItemsByKey, subscriptiontestutils.ExampleAddonRateCard3.Key()) |
|
|
| continue |
| } |
|
|
| b, _ := json.MarshalIndent(p, "", " ") |
|
|
| items, ok := p.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard3.Key()] |
| require.True(t, ok, "item missing in phase %s, got %+v, \nfull: %s", p.PhaseKey, lo.Keys(p.ItemsByKey), b) |
| require.Len(t, items, 1) |
|
|
| item := items[0] |
|
|
| pCad, err := spec.GetPhaseCadence(p.PhaseKey) |
| require.NoError(t, err) |
| cad := item.GetCadence(pCad) |
|
|
| |
| require.True(t, effPer.AsPeriod().IsSupersetOf(cad.AsPeriod()), "phase %s: item %s not in effective period %s", p.PhaseKey, cad.AsPeriod(), effPer.AsPeriod()) |
|
|
| |
| require.True(t, pCad.AsPeriod().IsSupersetOf(cad.AsPeriod()), "phase %s: item %s not in phase %s", p.PhaseKey, cad.AsPeriod(), pCad.AsPeriod()) |
|
|
| |
| switch pIdx { |
| case 0: |
| require.True(t, cad.ActiveFrom.Equal(subAddCadence.ActiveFrom)) |
| require.True(t, cad.ActiveTo.Equal(*pCad.ActiveTo)) |
| case 1: |
| require.True(t, cad.ActiveFrom.Equal(pCad.ActiveFrom)) |
| require.True(t, cad.ActiveTo.Equal(*subAddCadence.ActiveTo)) |
| } |
| } |
| }) |
| }) |
|
|
| t.Run("Should add a boolean entitlement to an existing item without entitlement with proper annotations", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| env := buildSubAndAddon( |
| t, |
| &deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard5ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, productcatalog.EffectivePeriod{ |
| EffectiveFrom: &now, |
| }, productcatalog.AddonInstanceTypeMultiple, subscriptiontestutils.ExampleAddonRateCard6.Clone()), |
| models.CadencedModel{ |
| ActiveFrom: now, |
| ActiveTo: nil, |
| }, |
| ) |
|
|
| spec := env.subView.Spec |
|
|
| |
| err := spec.Apply(env.diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| items := spec.Phases["test_phase_1"].ItemsByKey[subscriptiontestutils.ExampleRateCard5ForAddons.Key()] |
| require.Len(t, items, 1) |
|
|
| item := items[0] |
|
|
| require.Equal(t, 1, subscription.AnnotationParser.GetBooleanEntitlementCount(item.Annotations)) |
| }) |
| }) |
|
|
| t.Run("Should increment boolean entitlement count annotation", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| env := buildSubAndAddon( |
| t, |
| &deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard4ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, productcatalog.EffectivePeriod{ |
| EffectiveFrom: &now, |
| }, productcatalog.AddonInstanceTypeMultiple, subscriptiontestutils.ExampleAddonRateCard6.Clone()), |
| models.CadencedModel{ |
| ActiveFrom: now, |
| ActiveTo: nil, |
| }, |
| ) |
|
|
| spec := env.subView.Spec |
|
|
| item := spec.Phases["test_phase_1"].ItemsByKey[subscriptiontestutils.ExampleAddonRateCard6.Key()][0] |
| require.Equal(t, 1, subscription.AnnotationParser.GetBooleanEntitlementCount(item.Annotations)) |
|
|
| |
| err := spec.Apply(env.diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| items := spec.Phases["test_phase_1"].ItemsByKey[subscriptiontestutils.ExampleAddonRateCard6.Key()] |
| require.Len(t, items, 1) |
|
|
| item = items[0] |
|
|
| require.Equal(t, 2, subscription.AnnotationParser.GetBooleanEntitlementCount(item.Annotations)) |
| }) |
| }) |
|
|
| t.Run("Should add a new item for new key with boolean entitlement with proper annotations", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| env := buildSubAndAddon( |
| t, |
| &deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard2.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, productcatalog.EffectivePeriod{ |
| EffectiveFrom: &now, |
| }, productcatalog.AddonInstanceTypeMultiple, subscriptiontestutils.ExampleAddonRateCard6.Clone()), |
| models.CadencedModel{ |
| ActiveFrom: now, |
| ActiveTo: nil, |
| }, |
| ) |
|
|
| spec := env.subView.Spec |
|
|
| |
| err := spec.Apply(env.diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| items := spec.Phases["test_phase_1"].ItemsByKey[subscriptiontestutils.ExampleAddonRateCard6.Key()] |
| require.Len(t, items, 1) |
|
|
| item := items[0] |
|
|
| require.Equal(t, 1, subscription.AnnotationParser.GetBooleanEntitlementCount(item.Annotations)) |
| }) |
| }) |
|
|
| t.Run("Should update an existing Item that fills its entire phase", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| effPer := productcatalog.EffectivePeriod{ |
| EffectiveFrom: lo.ToPtr(now.AddDate(0, 0, 0)), |
| EffectiveTo: nil, |
| } |
|
|
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(lo.ToPtr(datetime.MustParseDuration(t, "P1M")), subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| effPer, |
| productcatalog.AddonInstanceTypeSingle, |
| subscriptiontestutils.ExampleAddonRateCard4.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| taxCodeID := lookupTaxCodeID(t, deps.deps, "txcd_10000000") |
| rc3 := withTaxCodeID(&subscriptiontestutils.ExampleRateCard3ForAddons, taxCodeID) |
|
|
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, models.CadencedModel{ |
| ActiveFrom: *effPer.EffectiveFrom, |
| ActiveTo: effPer.EffectiveTo, |
| }) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| for _, p := range spec.GetSortedPhases() { |
| items, ok := p.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()] |
| require.True(t, ok, "item missing in phase %s, got %+v", p.PhaseKey, lo.Keys(p.ItemsByKey)) |
| require.Len(t, items, 1) |
|
|
| item := items[0] |
|
|
| pc, err := spec.GetPhaseCadence(p.PhaseKey) |
| require.NoError(t, err) |
|
|
| cad := item.GetCadence(pc) |
|
|
| require.True(t, cad.ActiveFrom.Equal(pc.ActiveFrom)) |
|
|
| compareRateCardsWithAmountChange(t, rc3, 100*2, item.RateCard, "phase %s", p.PhaseKey) |
| } |
| }) |
| }) |
|
|
| t.Run("Should partially update an existing item in accordance with the cadence", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| effPer := productcatalog.EffectivePeriod{ |
| EffectiveFrom: lo.ToPtr(now), |
| EffectiveTo: nil, |
| } |
|
|
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(lo.ToPtr(datetime.MustParseDuration(t, "P1M")), subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| effPer, |
| productcatalog.AddonInstanceTypeSingle, |
| subscriptiontestutils.ExampleAddonRateCard4.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| taxCodeID := lookupTaxCodeID(t, deps.deps, "txcd_10000000") |
| rc3 := withTaxCodeID(&subscriptiontestutils.ExampleRateCard3ForAddons, taxCodeID) |
|
|
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, models.CadencedModel{ |
| ActiveFrom: now.AddDate(0, 0, 5), |
| ActiveTo: effPer.EffectiveTo, |
| }) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| p1 := spec.GetSortedPhases()[0] |
|
|
| b, _ := json.MarshalIndent(p1, "", " ") |
|
|
| require.Contains(t, p1.ItemsByKey, subscriptiontestutils.ExampleAddonRateCard4.Key(), "looked for: %s, full: %s", subscriptiontestutils.ExampleAddonRateCard4.Key(), string(b)) |
| require.Len(t, p1.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()], 2) |
|
|
| items := p1.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()] |
|
|
| pCad, err := spec.GetPhaseCadence(p1.PhaseKey) |
| require.NoError(t, err) |
|
|
| |
| cad1 := items[0].GetCadence(pCad) |
| require.True(t, cad1.ActiveFrom.Equal(pCad.ActiveFrom)) |
| require.True(t, cad1.ActiveTo.Equal(now.AddDate(0, 0, 5))) |
|
|
| cad2 := items[1].GetCadence(pCad) |
| require.True(t, cad2.ActiveFrom.Equal(now.AddDate(0, 0, 5))) |
| require.True(t, cad2.ActiveTo.Equal(*pCad.ActiveTo)) |
|
|
| |
| compareRateCardsWithAmountChange(t, rc3, 100, items[0].RateCard, "phase %s", p1.PhaseKey) |
|
|
| |
| compareRateCardsWithAmountChange(t, rc3, 100*2, items[1].RateCard, "phase %s", p1.PhaseKey) |
| }) |
| }) |
|
|
| t.Run("Should partially update an existing item in accordance with the cadence - but for multi instance", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| effPer := productcatalog.EffectivePeriod{ |
| EffectiveFrom: lo.ToPtr(now), |
| EffectiveTo: nil, |
| } |
|
|
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(lo.ToPtr(datetime.MustParseDuration(t, "P1M")), subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| effPer, |
| productcatalog.AddonInstanceTypeSingle, |
| subscriptiontestutils.ExampleAddonRateCard4.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| taxCodeID := lookupTaxCodeID(t, deps.deps, "txcd_10000000") |
| rc3 := withTaxCodeID(&subscriptiontestutils.ExampleRateCard3ForAddons, taxCodeID) |
|
|
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, models.CadencedModel{ |
| ActiveFrom: now.AddDate(0, 0, 5), |
| ActiveTo: effPer.EffectiveTo, |
| }) |
|
|
| |
| val := subsAdd.Quantities.GetAt(0).GetValue() |
|
|
| val.Quantity = 4 |
|
|
| subsAdd.Quantities = timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{ |
| val.AsTimed(), |
| }) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| { |
| p1 := spec.GetSortedPhases()[0] |
| require.Contains(t, p1.ItemsByKey, subscriptiontestutils.ExampleAddonRateCard4.Key()) |
| require.Len(t, p1.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()], 2) |
|
|
| items := p1.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()] |
|
|
| pCad, err := spec.GetPhaseCadence(p1.PhaseKey) |
| require.NoError(t, err) |
|
|
| |
| cad1 := items[0].GetCadence(pCad) |
| require.True(t, cad1.ActiveFrom.Equal(pCad.ActiveFrom)) |
| require.True(t, cad1.ActiveTo.Equal(now.AddDate(0, 0, 5))) |
|
|
| cad2 := items[1].GetCadence(pCad) |
| require.True(t, cad2.ActiveFrom.Equal(now.AddDate(0, 0, 5))) |
| require.True(t, cad2.ActiveTo.Equal(*pCad.ActiveTo)) |
|
|
| |
| compareRateCardsWithAmountChange(t, rc3, 100, items[0].RateCard, "phase %s", p1.PhaseKey) |
|
|
| |
| compareRateCardsWithAmountChange(t, rc3, 100*(1+4), items[1].RateCard, "phase %s", p1.PhaseKey) |
| } |
|
|
| |
| { |
| p2 := spec.GetSortedPhases()[1] |
| require.Contains(t, p2.ItemsByKey, subscriptiontestutils.ExampleAddonRateCard4.Key()) |
| items := p2.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()] |
|
|
| require.Len(t, items, 1) |
|
|
| item := items[0] |
|
|
| pCad, err := spec.GetPhaseCadence(p2.PhaseKey) |
| require.NoError(t, err) |
|
|
| cad := item.GetCadence(pCad) |
| require.True(t, cad.ActiveFrom.Equal(pCad.ActiveFrom)) |
| require.Nil(t, cad.ActiveTo) |
|
|
| compareRateCardsWithAmountChange(t, rc3, 100*(1+4), item.RateCard, "phase %s", p2.PhaseKey) |
| } |
| }) |
| }) |
|
|
| t.Run("Should guarantee access is continuous across changing items", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| t0 := now |
| clock.FreezeTime(t0.Add(time.Millisecond)) |
|
|
| defer clock.UnFreeze() |
|
|
| t1 := now.AddDate(0, 0, 5) |
|
|
| t2 := t1.AddDate(0, 1, 5) |
|
|
| t3 := t2.AddDate(0, 0, 6) |
|
|
| t4 := t3.AddDate(0, 0, 9) |
|
|
| effPer := productcatalog.EffectivePeriod{ |
| EffectiveFrom: &now, |
| } |
|
|
| subAddCadence := models.CadencedModel{ |
| ActiveFrom: t1, |
| ActiveTo: lo.ToPtr(t4), |
| } |
|
|
| p, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| effPer, |
| productcatalog.AddonInstanceTypeSingle, |
| subscriptiontestutils.ExampleAddonRateCard4.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, p, now) |
|
|
| _, err := deps.deps.SubscriptionService.GetView(context.Background(), subView.Subscription.NamespacedID) |
| require.NoError(t, err) |
|
|
| subsAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, subAddCadence) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subsAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| |
| ogItem := spec.Phases["test_phase_1"].ItemsByKey[subscriptiontestutils.ExampleRateCard3ForAddons.Key()][0] |
| spec.Phases["test_phase_1"].ItemsByKey[subscriptiontestutils.ExampleRateCard3ForAddons.Key()] = []*subscription.SubscriptionItemSpec{ |
| |
| { |
| CreateSubscriptionItemInput: subscription.CreateSubscriptionItemInput{ |
| CreateSubscriptionItemPlanInput: subscription.CreateSubscriptionItemPlanInput{ |
| PhaseKey: spec.Phases["test_phase_1"].PhaseKey, |
| ItemKey: subscriptiontestutils.ExampleRateCard3ForAddons.Key(), |
| RateCard: subscriptiontestutils.ExampleRateCard3ForAddons.Clone(), |
| }, |
| CreateSubscriptionItemCustomerInput: subscription.CreateSubscriptionItemCustomerInput{ |
| ActiveToOverrideRelativeToPhaseStart: lo.ToPtr(datetime.ISODurationBetween(t0, t2)), |
| }, |
| Annotations: ogItem.Annotations, |
| }, |
| }, |
| |
| { |
| CreateSubscriptionItemInput: subscription.CreateSubscriptionItemInput{ |
| CreateSubscriptionItemPlanInput: subscription.CreateSubscriptionItemPlanInput{ |
| PhaseKey: spec.Phases["test_phase_1"].PhaseKey, |
| ItemKey: subscriptiontestutils.ExampleRateCard3ForAddons.Key(), |
| RateCard: subscriptiontestutils.ExampleRateCard3ForAddons.Clone(), |
| }, |
| CreateSubscriptionItemCustomerInput: subscription.CreateSubscriptionItemCustomerInput{ |
| ActiveFromOverrideRelativeToPhaseStart: lo.ToPtr(datetime.ISODurationBetween(t0, t3)), |
| }, |
| Annotations: ogItem.Annotations, |
| }, |
| }, |
| } |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| |
| key := subscriptiontestutils.ExampleRateCard3ForAddons.Key() |
|
|
| phase := spec.GetSortedPhases()[0] |
| pCad, err := spec.GetPhaseCadence(phase.PhaseKey) |
| require.NoError(t, err) |
|
|
| require.Contains(t, phase.ItemsByKey, key) |
| require.Len(t, phase.ItemsByKey[key], 5) |
| items := phase.ItemsByKey[key] |
| |
| require.True(t, items[0].GetCadence(pCad).ActiveFrom.Equal(t0)) |
| require.True(t, items[0].GetCadence(pCad).ActiveTo.Equal(t1)) |
| compareRateCardsWithAmountChange(t, &subscriptiontestutils.ExampleRateCard3ForAddons, 100, items[0].RateCard, "phase %s", phase.PhaseKey) |
|
|
| |
| require.True(t, items[1].GetCadence(pCad).ActiveFrom.Equal(t1)) |
| require.True(t, items[1].GetCadence(pCad).ActiveTo.Equal(t2)) |
| compareRateCardsWithAmountChange(t, &subscriptiontestutils.ExampleRateCard3ForAddons, 100*2, items[1].RateCard, "phase %s", phase.PhaseKey) |
| require.Equal(t, ogItem.Annotations, items[1].Annotations, "annotations should be the same") |
|
|
| |
| require.True(t, items[2].GetCadence(pCad).ActiveFrom.Equal(t2)) |
| require.True(t, items[2].GetCadence(pCad).ActiveTo.Equal(t3)) |
| compareRateCardsWithAmountChange(t, subsAdd.RateCards[0].AddonRateCard.RateCard.(*productcatalog.FlatFeeRateCard), 100, items[2].RateCard, "phase %s", phase.PhaseKey) |
|
|
| |
| require.True(t, items[3].GetCadence(pCad).ActiveFrom.Equal(t3)) |
| require.True(t, items[3].GetCadence(pCad).ActiveTo.Equal(t4)) |
| compareRateCardsWithAmountChange(t, &subscriptiontestutils.ExampleRateCard3ForAddons, 100*2, items[3].RateCard, "phase %s", phase.PhaseKey) |
| require.Equal(t, ogItem.Annotations, items[3].Annotations, "annotations should be the same") |
|
|
| |
| require.True(t, items[4].GetCadence(pCad).ActiveFrom.Equal(t4)) |
| require.Nil(t, items[4].GetCadence(pCad).ActiveTo) |
| compareRateCardsWithAmountChange(t, &subscriptiontestutils.ExampleRateCard3ForAddons, 100, items[4].RateCard, "phase %s", phase.PhaseKey) |
| }) |
| }) |
|
|
| t.Run("Should create multiple rate cards in the addon", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| |
| effPer := productcatalog.EffectivePeriod{ |
| EffectiveFrom: lo.ToPtr(now), |
| EffectiveTo: nil, |
| } |
|
|
| pl, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| effPer, |
| productcatalog.AddonInstanceTypeMultiple, |
| subscriptiontestutils.ExampleAddonRateCard4.Clone(), |
| subscriptiontestutils.ExampleAddonRateCard3.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, pl, now) |
|
|
| taxCodeID := lookupTaxCodeID(t, deps.deps, "txcd_10000000") |
| rc3 := withTaxCodeID(&subscriptiontestutils.ExampleRateCard3ForAddons, taxCodeID) |
|
|
| subAdd := subscriptiontestutils.CreateAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, models.CadencedModel{ |
| ActiveFrom: *effPer.EffectiveFrom, |
| ActiveTo: effPer.EffectiveTo, |
| }) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| p, ok := spec.Phases["test_phase_1"] |
| require.True(t, ok) |
|
|
| pCad, err := spec.GetPhaseCadence(p.PhaseKey) |
| require.NoError(t, err) |
|
|
| |
| require.Contains(t, p.ItemsByKey, subscriptiontestutils.ExampleRateCard3ForAddons.Key()) |
|
|
| items := p.ItemsByKey[subscriptiontestutils.ExampleRateCard3ForAddons.Key()] |
| require.Len(t, items, 1) |
|
|
| item := items[0] |
|
|
| require.True(t, item.GetCadence(pCad).ActiveFrom.Equal(now)) |
| require.Nil(t, item.GetCadence(pCad).ActiveTo) |
|
|
| compareRateCardsWithAmountChange(t, rc3, 100*2, item.RateCard, "phase %s", p.PhaseKey) |
|
|
| |
| require.Contains(t, p.ItemsByKey, subscriptiontestutils.ExampleAddonRateCard3.Key()) |
|
|
| items = p.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard3.Key()] |
| require.Len(t, items, 1) |
|
|
| item = items[0] |
|
|
| require.True(t, item.GetCadence(pCad).ActiveFrom.Equal(now)) |
| require.Nil(t, item.GetCadence(pCad).ActiveTo) |
|
|
| compareRateCardsWithAmountChange(t, &subscriptiontestutils.ExampleAddonRateCard3, 100, item.RateCard, "phase %s", p.PhaseKey) |
| }) |
| }) |
| } |
|
|
| func TestApplyWithMultiInstance(t *testing.T) { |
| type tcDeps struct { |
| deps subscriptiontestutils.SubscriptionDependencies |
| } |
|
|
| runWithDeps := func(t *testing.T, fn func(t *testing.T, deps *tcDeps)) { |
| clock.FreezeTime(now.Add(time.Millisecond)) |
| defer func() { |
| clock.UnFreeze() |
| clock.ResetTime() |
| }() |
|
|
| dbDeps := subscriptiontestutils.SetupDBDeps(t) |
| defer dbDeps.Cleanup(t) |
|
|
| deps := subscriptiontestutils.NewService(t, dbDeps) |
|
|
| fn(t, &tcDeps{ |
| deps: deps, |
| }) |
| } |
|
|
| t.Run("Should create items in phase according to quantities", func(t *testing.T) { |
| runWithDeps(t, func(t *testing.T, deps *tcDeps) { |
| t0 := now |
|
|
| t1 := t0.AddDate(0, 0, 1) |
|
|
| t2 := t1.AddDate(0, 0, 1) |
|
|
| effPer := productcatalog.EffectivePeriod{ |
| EffectiveFrom: lo.ToPtr(now), |
| EffectiveTo: nil, |
| } |
|
|
| pl, a := subscriptiontestutils.CreatePlanWithAddon( |
| t, |
| deps.deps, |
| subscriptiontestutils.BuildTestPlanInput(t). |
| AddPhase(nil, subscriptiontestutils.ExampleRateCard3ForAddons.Clone()). |
| Build(), |
| subscriptiontestutils.BuildAddonForTesting(t, |
| effPer, |
| productcatalog.AddonInstanceTypeMultiple, |
| subscriptiontestutils.ExampleAddonRateCard4.Clone(), |
| ), |
| ) |
|
|
| subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps.deps, pl, now) |
|
|
| taxCodeID := lookupTaxCodeID(t, deps.deps, "txcd_10000000") |
| rc3 := withTaxCodeID(&subscriptiontestutils.ExampleRateCard3ForAddons, taxCodeID) |
|
|
| subAdd := subscriptiontestutils.CreateMultiInstanceAddonForSubscription(t, &deps.deps, subView.Subscription.NamespacedID, a.NamespacedID, []subscriptionaddon.CreateSubscriptionAddonQuantityInput{ |
| { |
| ActiveFrom: t1, |
| Quantity: 1, |
| }, |
| { |
| ActiveFrom: t2, |
| Quantity: 2, |
| }, |
| }) |
|
|
| diffable, err := addondiff.GetDiffableFromAddon(subView, subAdd) |
| require.NoError(t, err) |
|
|
| spec := subView.Spec |
|
|
| err = spec.Apply(diffable.GetApplies(), subscription.ApplyContext{CurrentTime: now}) |
| require.NoError(t, err) |
|
|
| p, ok := spec.Phases["test_phase_1"] |
| require.True(t, ok) |
|
|
| pCad, err := spec.GetPhaseCadence(p.PhaseKey) |
| require.NoError(t, err) |
|
|
| require.Contains(t, p.ItemsByKey, subscriptiontestutils.ExampleAddonRateCard4.Key()) |
| require.Len(t, p.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()], 3) |
|
|
| items := p.ItemsByKey[subscriptiontestutils.ExampleAddonRateCard4.Key()] |
|
|
| |
| require.True(t, items[0].GetCadence(pCad).ActiveFrom.Equal(t0)) |
| require.True(t, items[0].GetCadence(pCad).ActiveTo.Equal(t1)) |
| compareRateCardsWithAmountChange(t, rc3, 100*1, items[0].RateCard, "phase %s", p.PhaseKey) |
|
|
| |
| require.True(t, items[1].GetCadence(pCad).ActiveFrom.Equal(t1)) |
| require.True(t, items[1].GetCadence(pCad).ActiveTo.Equal(t2)) |
|
|
| compareRateCardsWithAmountChange(t, rc3, 100*2, items[1].RateCard, "phase %s", p.PhaseKey) |
|
|
| |
| require.True(t, items[2].GetCadence(pCad).ActiveFrom.Equal(t2)) |
| require.Nil(t, items[2].GetCadence(pCad).ActiveTo) |
|
|
| compareRateCardsWithAmountChange(t, rc3, 100*3, items[2].RateCard, "phase %s", p.PhaseKey) |
| }) |
| }) |
| } |
|
|
| |
| |
| func lookupTaxCodeID(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies, stripeCode string) *string { |
| t.Helper() |
|
|
| tc, err := deps.TaxCodeService.GetTaxCodeByAppMapping(context.Background(), taxcode.GetTaxCodeByAppMappingInput{ |
| Namespace: subscriptiontestutils.ExampleNamespace, |
| AppType: app.AppTypeStripe, |
| TaxCode: stripeCode, |
| }) |
| require.NoError(t, err) |
|
|
| return &tc.ID |
| } |
|
|
| |
| func withTaxCodeID(rc *productcatalog.FlatFeeRateCard, taxCodeID *string) *productcatalog.FlatFeeRateCard { |
| clone := rc.Clone().(*productcatalog.FlatFeeRateCard) |
| if clone.TaxConfig != nil { |
| clone.TaxConfig.TaxCodeID = taxCodeID |
| } |
|
|
| return clone |
| } |
|
|
| func compareRateCardsWithAmountChange(t *testing.T, baseTarget *productcatalog.FlatFeeRateCard, targetAmount int, value productcatalog.RateCard, msgAndArgs ...interface{}) { |
| t.Helper() |
|
|
| targetMeta := baseTarget.RateCardMeta.Clone() |
|
|
| targetMeta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(int64(targetAmount)), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: targetMeta, |
| BillingCadence: baseTarget.BillingCadence, |
| } |
|
|
| b1, _ := json.MarshalIndent(value, "", " ") |
| b2, _ := json.MarshalIndent(target, "", " ") |
|
|
| msgX := fmt.Sprintf("item %s not equal to target %s", b1, b2) |
|
|
| left := target.Clone() |
| left.SetFeature(nil, left.GetFeatureKey()) |
|
|
| right := value.Clone() |
| right.SetFeature(nil, right.GetFeatureKey()) |
|
|
| if len(msgAndArgs) > 0 { |
| customMsg := fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) |
| require.True(t, left.Equal(right), "%s: %s", customMsg, msgX) |
| } else { |
| require.True(t, left.Equal(right), msgX) |
| } |
| } |
|
|