File size: 13,925 Bytes
16cdcb7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | 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"
)
// Ensures the created view matches the input spec
func ValidateSpecAndView(t *testing.T, expected subscription.SubscriptionSpec, found subscription.SubscriptionView) {
t.Helper()
// Let's validate the Subscription itself
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)
// Let's validate the phases
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]
// Let's validate the phase properties
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())
// Test Rate Cards of Phase
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]
// Let's validate the item properties
assert.Equal(t, specItem.ItemKey, foundItem.SubscriptionItem.Key)
// Validate phase linking both ways
assert.Equal(t, foundPhase.SubscriptionPhase.Key, specItem.PhaseKey)
assert.Equal(t, foundPhase.SubscriptionPhase.ID, foundItem.SubscriptionItem.PhaseId)
// Let's validate the RateCard is equal
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)
// Let's validate the TaxCodeID
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)
}
// Let's validate the Feature linking
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
// Let's validate the Entitlement
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())
// Let's validate that subscriptionID annotation is present
assert.Equal(t, foundItem.Entitlement.Entitlement.Annotations[subscription.AnnotationSubscriptionID], found.Subscription.NamespacedID.ID)
// Let's validate that the UsagePeriod is aligned
period := GetEntitlementTemplateUsagePeriod(t, *specItem.RateCard.AsMeta().EntitlementTemplate)
require.NotNil(t, period)
// Entitlement UsagePeriod should be aligned to the subscription billing anchor, which means
truncatedBillingAnchor := found.Subscription.BillingAnchor.Truncate(time.Minute) // Due to minute precision
// - its duration should be identical
entPeriod := ent.Entitlement.UsagePeriod.GetOriginalValueAsUsagePeriodInput().GetValue().Interval.ISODuration
assert.True(t, entPeriod.Equal(period), "usage period interval mismatch, expected %s, got %s", period, entPeriod)
// - its anchor should be "aligned" with the subscription's billingAnchor
require.NotNil(t, ent.Entitlement.UsagePeriod)
// billinganchor would be in the past compared to entitlement start, so usageperiod would normalize to a later iteration
// to avoid that, lets test with recurrence instead
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:
// Validate measureUsageFrom, it should measure usage form the start of the current phase
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))
}
// Validate that entitlement activeFrom is the same as the item activeFrom
require.NotNil(t, ent.Entitlement.ActiveFrom)
assert.Equal(t, foundItem.SubscriptionItem.ActiveFrom, *ent.Entitlement.ActiveFrom)
// Validate that the entitlement is only active until the item is scheduled to be
assert.Equal(t, foundItem.SubscriptionItem.ActiveTo, ent.Entitlement.ActiveTo)
} else {
// If an entitlement wasn't defined then there shouldn't be an entitlement
assert.Nil(t, foundItem.Entitlement)
}
}
}
}
}
func SpecsEqual(t *testing.T, s1, s2 subscription.SubscriptionSpec) {
t.Helper()
// Let's validate the Subscription itself
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)
// Let's validate the phases
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)
// Let's validate the phase properties
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)
// Let's validate the items
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]
// Let's validate the item properties
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)
// We'll compare the time offsets separately
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,
)
// Then we compare the rest without the offsets
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) // TODO: check all fields?
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())
}
}
|