Pulse/pkg/licensing/billing_state_normalization.go
2026-03-18 16:06:30 +00:00

91 lines
3.3 KiB
Go

package licensing
import "strings"
func DefaultBillingState() *BillingState {
return &BillingState{
Capabilities: []string{},
Limits: map[string]int64{},
MetersEnabled: []string{},
PlanVersion: string(SubStateTrial),
SubscriptionState: SubStateTrial,
}
}
func NormalizeBillingState(state *BillingState) *BillingState {
if state == nil {
return DefaultBillingState()
}
// Start with a full struct copy so new fields are never silently dropped.
cp := *state
normalized := &cp
// Deep-clone reference types to avoid aliasing the original.
normalized.Capabilities = append([]string(nil), state.Capabilities...)
normalized.MetersEnabled = append([]string(nil), state.MetersEnabled...)
normalized.Limits = make(map[string]int64, len(state.Limits))
for key, value := range state.Limits {
normalized.Limits[key] = value
}
// Clone pointer fields so the caller can't mutate through the original.
normalized.TrialStartedAt = cloneInt64Ptr(state.TrialStartedAt)
normalized.TrialEndsAt = cloneInt64Ptr(state.TrialEndsAt)
normalized.TrialExtendedAt = cloneInt64Ptr(state.TrialExtendedAt)
normalized.OverflowGrantedAt = cloneInt64Ptr(state.OverflowGrantedAt)
normalized.QuickstartCreditsGrantedAt = cloneInt64Ptr(state.QuickstartCreditsGrantedAt)
normalized.CommercialMigration = CloneCommercialMigrationStatus(state.CommercialMigration)
// Normalize string fields.
normalized.PlanVersion = strings.TrimSpace(normalized.PlanVersion)
normalized.SubscriptionState = SubscriptionState(strings.ToLower(strings.TrimSpace(string(normalized.SubscriptionState))))
normalized.EntitlementJWT = strings.TrimSpace(normalized.EntitlementJWT)
normalized.EntitlementRefreshToken = strings.TrimSpace(normalized.EntitlementRefreshToken)
normalized.StripeCustomerID = strings.TrimSpace(normalized.StripeCustomerID)
normalized.StripeSubscriptionID = strings.TrimSpace(normalized.StripeSubscriptionID)
normalized.StripePriceID = strings.TrimSpace(normalized.StripePriceID)
normalized.CommercialMigration = NormalizeCommercialMigrationStatus(normalized.CommercialMigration)
normalized.Limits = NormalizeMonitoredSystemLimits(normalized.Limits)
// Ensure slices/maps are never nil (JSON marshals as [] / {} instead of null).
if normalized.Capabilities == nil {
normalized.Capabilities = []string{}
}
if normalized.Limits == nil {
normalized.Limits = map[string]int64{}
}
if normalized.MetersEnabled == nil {
normalized.MetersEnabled = []string{}
}
// Preserve absence when the stored hosted billing record has no plan label.
// Canonical defaults still come from DefaultBillingState()/call-site defaults.
normalized.PlanVersion = CanonicalizePlanVersion(normalized.PlanVersion)
switch normalized.SubscriptionState {
case SubStateExpired, SubStateSuspended, SubStateCanceled:
normalized.Capabilities = []string{}
normalized.Limits = map[string]int64{}
normalized.MetersEnabled = []string{}
default:
if limit, known := CloudPlanMonitoredSystemLimits[normalized.PlanVersion]; known {
normalized.Limits[MaxMonitoredSystemsLicenseGateKey] = int64(limit)
}
}
return normalized
}
func IsValidBillingSubscriptionState(state SubscriptionState) bool {
switch state {
case SubStateTrial,
SubStateActive,
SubStateGrace,
SubStateExpired,
SubStateSuspended,
SubStateCanceled:
return true
default:
return false
}
}