From 4db2914c9208aec142a286e53924eb191782d886 Mon Sep 17 00:00:00 2001 From: Sarah Roberts Date: Mon, 28 Oct 2024 18:25:16 -0700 Subject: [PATCH] CORE-2016: updated the subscription endpoints to support plan rates and multiple scheduled plan quota default updates --- internal/controllers/admins.go | 1 + internal/db/subscriptions.go | 28 ++++++++++++--- internal/model/plan.go | 62 ++++++++++++++++++++++++++++------ 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/internal/controllers/admins.go b/internal/controllers/admins.go index 50c6e59..0e63510 100644 --- a/internal/controllers/admins.go +++ b/internal/controllers/admins.go @@ -24,6 +24,7 @@ func (s Server) GetAllActiveSubscriptions(ctx echo.Context) error { Preload("Quotas.ResourceType"). Preload("Usages"). Preload("Usages.ResourceType"). + Preload("PlanRate"). Where( s.GORMDB.WithContext(context). Where("CURRENT_TIMESTAMP BETWEEN subscriptions.effective_start_date AND subscriptions.effective_end_date"). diff --git a/internal/db/subscriptions.go b/internal/db/subscriptions.go index cd4d379..fc7d6f8 100644 --- a/internal/db/subscriptions.go +++ b/internal/db/subscriptions.go @@ -12,20 +12,29 @@ import ( "gorm.io/gorm/clause" ) -// QuotasFromPlan generates a set of quotas from the plan quota defaults in a plan. This function assumes that the -// given plan already contains the plan quota defaults. +// QuotasFromPlan generates a set of quotas from the plan quota defaults in a plan. func QuotasFromPlan(plan *model.Plan, periods int32) []model.Quota { - result := make([]model.Quota, len(plan.PlanQuotaDefaults)) - for i, quotaDefault := range plan.PlanQuotaDefaults { + + // Get the active plan quota defaults from the plan. + pqds := plan.GetDefaultQuotaValues() + + // Build the array of quotas. + result := make([]model.Quota, len(pqds)) + + // Populate the quotas. + currentIndex := 0 + for _, quotaDefault := range pqds { quotaValue := quotaDefault.QuotaValue if quotaDefault.ResourceType.Consumable { quotaValue *= float64(periods) } - result[i] = model.Quota{ + result[currentIndex] = model.Quota{ Quota: quotaValue, ResourceTypeID: quotaDefault.ResourceTypeID, } + currentIndex++ } + return result } @@ -36,6 +45,12 @@ func SubscribeUserToPlan( wrapMsg := "unable to add user plan" var err error + // Look up the active plan rate. + planRate, err := plan.GetActivePlanRate() + if err != nil { + return nil, errors.Wrap(err, wrapMsg) + } + // Define the user plan. effectiveStartDate := time.Now() effectiveEndDate := opts.GetEndDate(effectiveStartDate) @@ -46,6 +61,7 @@ func SubscribeUserToPlan( PlanID: plan.ID, Quotas: QuotasFromPlan(plan, opts.GetPeriods()), Paid: opts.IsPaid(), + PlanRateID: planRate.ID, } err = db.WithContext(ctx).Create(&subscription).Error if err != nil { @@ -148,6 +164,7 @@ func GetSubscriptionDetails(ctx context.Context, db *gorm.DB, subscriptionID str Preload("Quotas.ResourceType"). Preload("Usages"). Preload("Usages.ResourceType"). + Preload("PlanRate"). Where("id = ?", subscriptionID). First(&subscription). Error @@ -201,6 +218,7 @@ func ListSubscriptions(ctx context.Context, db *gorm.DB, params *SubscriptionLis Preload("Quotas.ResourceType"). Preload("Usages"). Preload("Usages.ResourceType"). + Preload("PlanRate"). Where( db.Where("CURRENT_TIMESTAMP BETWEEN subscriptions.effective_start_date AND subscriptions.effective_end_date"). Or("CURRENT_TIMESTAMP > subscriptions.effective_start_date AND subscriptions.effective_end_date IS NULL"), diff --git a/internal/model/plan.go b/internal/model/plan.go index aafcb11..a23d69a 100644 --- a/internal/model/plan.go +++ b/internal/model/plan.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "time" ) @@ -28,30 +29,65 @@ type Plan struct { PlanRates []PlanRate `json:"plan_rates,omitempty"` } -// GetCurrentRate returns the current rate associated with the plan. -func (p *Plan) GetCurrentRate() float64 { - var rate float64 - currentDate := time.Now() - for _, planRate := range p.PlanRates { - if !planRate.EffectiveDate.After(currentDate) { - rate = planRate.Rate +// Returns the currently active rate for a subscription plan. The active plan rate is the plan with the most recent +// effective timestamp that occurs at or befor the curren time. This function assumes that the plan quota defaults are +// sorted in ascending order by effective date. +func (p *Plan) GetActivePlanRate() (*PlanRate, error) { + currentTime := time.Now() + + // Find the active plan rate. + var activePlanRate *PlanRate + for _, pr := range p.PlanRates { + if pr.EffectiveDate.After(currentTime) { + break } + activePlanRate = &pr } - return rate + + // It's an error for a plan not to have an active rate. + if activePlanRate == nil { + return nil, fmt.Errorf("no active rate found for subscription plan %s", *p.ID) + } + + return activePlanRate, nil } -// GetDefaultQuotaValue returns the default quota value associated with the resource type with the given name. +// GetDefaultQuotaValue returns the default quota value associated with the resource type with the given name. This +// funciton assumes that the plan quota defaults ar sorted in ascending order by effective date. func (p *Plan) GetDefaultQuotaValue(resourcetypeName string) float64 { + currentTime := time.Now() + + // Find the active plan quota default value for the given resource type. var value float64 for _, quotaDefault := range p.PlanQuotaDefaults { + if quotaDefault.EffectiveDate.After(currentTime) { + break + } if quotaDefault.ResourceType.Name == resourcetypeName { value = quotaDefault.QuotaValue - break } } + return value } +// GetActiveQuotaValues returns the active quota values for a plan. This function assumes that the plan quota defaults +// are sorted in ascending order by effective date. +func (p *Plan) GetDefaultQuotaValues() map[string]*PlanQuotaDefault { + currentTime := time.Now() + + // Find the active plan quota defaults for each resource type. + result := make(map[string]*PlanQuotaDefault) + for _, planQuotaDefault := range p.PlanQuotaDefaults { + if planQuotaDefault.EffectiveDate.After(currentTime) { + break + } + result[planQuotaDefault.ResourceType.Name] = &planQuotaDefault + } + + return result +} + // PlanQuotaDefault define the structure for an Api Plan and Quota. type PlanQuotaDefault struct { // The plan quota default identifier @@ -135,6 +171,12 @@ type Subscription struct { // True if the user paid for the subscription. Paid bool `json:"paid"` + + // The ID of the plan rate at the time the subscription was created. + PlanRateID *string `gorm:"type:uuid;not null" json:"-"` + + // The plan rate at the time the subscription was created. + PlanRate *PlanRate `json:"plan_rate,omitempty"` } // GetCurrentUsageValue returns the current usage value for the resource type with the given resource type ID. Be