mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 08:33:03 +00:00
121 lines
4.3 KiB
Go
121 lines
4.3 KiB
Go
package licensing
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Transition represents a valid state transition.
|
|
type Transition struct {
|
|
From SubscriptionState
|
|
To SubscriptionState
|
|
}
|
|
|
|
// validTransitions defines all allowed state transitions.
|
|
var validTransitions = map[Transition]bool{
|
|
{SubStateTrial, SubStateActive}: true, // Trial converted to paid
|
|
{SubStateTrial, SubStateExpired}: true, // Trial expired without conversion
|
|
{SubStateActive, SubStateGrace}: true, // Payment failed, entering grace
|
|
{SubStateActive, SubStateSuspended}: true, // Admin suspension
|
|
{SubStateGrace, SubStateActive}: true, // Payment recovered
|
|
{SubStateGrace, SubStateExpired}: true, // Grace period ended
|
|
{SubStateExpired, SubStateActive}: true, // Re-subscription
|
|
{SubStateSuspended, SubStateActive}: true, // Admin unsuspension
|
|
{SubStateSuspended, SubStateExpired}: true, // Suspension expired to full expiry
|
|
}
|
|
|
|
// CanTransition checks if a transition from one state to another is valid.
|
|
func CanTransition(from, to SubscriptionState) bool {
|
|
return validTransitions[Transition{from, to}]
|
|
}
|
|
|
|
// ValidTransitionsFrom returns all valid target states from the given state.
|
|
func ValidTransitionsFrom(from SubscriptionState) []SubscriptionState {
|
|
targets := make([]SubscriptionState, 0)
|
|
for t := range validTransitions {
|
|
if t.From == from {
|
|
targets = append(targets, t.To)
|
|
}
|
|
}
|
|
|
|
// Stabilize ordering for deterministic callers/tests.
|
|
slices.Sort(targets)
|
|
return targets
|
|
}
|
|
|
|
// DowngradePolicy defines behavior when a subscription downgrades.
|
|
type DowngradePolicy struct {
|
|
// SoftHideGraceDays is the number of days data exceeding new limits
|
|
// is soft-hidden (accessible but flagged) after downgrade.
|
|
SoftHideGraceDays int
|
|
|
|
// HardDeleteAfterDays is the number of days after downgrade when
|
|
// data exceeding new limits is permanently deleted.
|
|
HardDeleteAfterDays int
|
|
}
|
|
|
|
// DefaultDowngradePolicy is the default policy for subscription downgrades.
|
|
var DefaultDowngradePolicy = DowngradePolicy{
|
|
SoftHideGraceDays: 30,
|
|
HardDeleteAfterDays: 60,
|
|
}
|
|
|
|
// DowngradeRetentionState is the durable local handoff between a reduced
|
|
// commercial history entitlement and storage retention. Access is restricted
|
|
// immediately by the entitlement evaluator; physical deletion cannot begin
|
|
// before PurgeEligibleAt.
|
|
type DowngradeRetentionState struct {
|
|
PreviousTier Tier `json:"previous_tier"`
|
|
CurrentTier Tier `json:"current_tier"`
|
|
PreviousHistoryDays int `json:"previous_history_days"`
|
|
CurrentHistoryDays int `json:"current_history_days"`
|
|
DetectedAt int64 `json:"detected_at"`
|
|
RecoveryGuaranteedTo int64 `json:"recovery_guaranteed_to"`
|
|
PurgeEligibleAt int64 `json:"purge_eligible_at"`
|
|
}
|
|
|
|
// NormalizeDowngradeRetentionState canonicalizes persisted downgrade state.
|
|
func NormalizeDowngradeRetentionState(state DowngradeRetentionState) DowngradeRetentionState {
|
|
state.PreviousTier = Tier(strings.ToLower(strings.TrimSpace(string(state.PreviousTier))))
|
|
state.CurrentTier = Tier(strings.ToLower(strings.TrimSpace(string(state.CurrentTier))))
|
|
return state
|
|
}
|
|
|
|
// AdvanceDowngradeRetention records a new history reduction, preserves an
|
|
// existing window for same-tier refreshes, and clears it on entitlement
|
|
// expansion before data has been purged.
|
|
func AdvanceDowngradeRetention(existing *DowngradeRetentionState, previousTier, currentTier Tier, now time.Time) *DowngradeRetentionState {
|
|
previousDays := historyDaysForDowngradeRetention(previousTier)
|
|
currentDays := historyDaysForDowngradeRetention(currentTier)
|
|
if currentDays >= previousDays {
|
|
if currentDays > previousDays {
|
|
return nil
|
|
}
|
|
if existing == nil {
|
|
return nil
|
|
}
|
|
preserved := NormalizeDowngradeRetentionState(*existing)
|
|
return &preserved
|
|
}
|
|
|
|
detectedAt := now.UTC().Unix()
|
|
state := &DowngradeRetentionState{
|
|
PreviousTier: previousTier,
|
|
CurrentTier: currentTier,
|
|
PreviousHistoryDays: previousDays,
|
|
CurrentHistoryDays: currentDays,
|
|
DetectedAt: detectedAt,
|
|
RecoveryGuaranteedTo: now.UTC().AddDate(0, 0, DefaultDowngradePolicy.SoftHideGraceDays).Unix(),
|
|
PurgeEligibleAt: now.UTC().AddDate(0, 0, DefaultDowngradePolicy.HardDeleteAfterDays).Unix(),
|
|
}
|
|
return state
|
|
}
|
|
|
|
func historyDaysForDowngradeRetention(tier Tier) int {
|
|
if days, ok := TierHistoryDays[tier]; ok && days > 0 {
|
|
return days
|
|
}
|
|
return TierHistoryDays[TierFree]
|
|
}
|