package domain import ( "strings" "time" ) type SubscriptionCategory string const ( SubscriptionCategoryLearnEnglish SubscriptionCategory = "LEARN_ENGLISH" SubscriptionCategoryIELTS SubscriptionCategory = "IELTS" SubscriptionCategoryDuolingo SubscriptionCategory = "DUOLINGO" ) // SubscriptionExpiryReminder is a Learn English subscription expiring in seven days. type SubscriptionExpiryReminder struct { UserID int64 PlanName string ExpiresAt time.Time } // CategorySubscriptionGateDisabled skips subscription enforcement on learner-facing routes (temporary). var CategorySubscriptionGateDisabled = false // ExamPrepSubscriptionGateDisabled skips IELTS/DUOLINGO subscription checks on exam-prep routes (temporary). var ExamPrepSubscriptionGateDisabled = true // IsLMSSubscriptionCategory reports plan categories that unlock Learn English (LMS) content. func IsLMSSubscriptionCategory(category string) bool { return normalizeSubscriptionCategory(category) == string(SubscriptionCategoryLearnEnglish) } // IsExamPrepSubscriptionCategory reports plan categories that unlock exam-prep content. func IsExamPrepSubscriptionCategory(category string) bool { switch normalizeSubscriptionCategory(category) { case string(SubscriptionCategoryIELTS), string(SubscriptionCategoryDuolingo): return true default: return false } } // IsLMSContentCategory reports content categories stored on LMS programs. func IsLMSContentCategory(category string) bool { return IsLMSSubscriptionCategory(category) } // IsExamPrepContentCategory reports content categories stored on exam-prep catalog courses. func IsExamPrepContentCategory(category string) bool { return IsExamPrepSubscriptionCategory(category) } // SubscriptionGrantsContentAccess reports whether an active plan category unlocks the given content category. func SubscriptionGrantsContentAccess(subscriptionCategory, contentCategory string) bool { sub := normalizeSubscriptionCategory(subscriptionCategory) content := normalizeSubscriptionCategory(contentCategory) if IsLMSContentCategory(content) { return sub == string(SubscriptionCategoryLearnEnglish) } if IsExamPrepContentCategory(content) { return sub == content } return false } func normalizeSubscriptionCategory(category string) string { return strings.ToUpper(strings.TrimSpace(category)) } type DurationUnit string const ( DurationUnitDay DurationUnit = "DAY" DurationUnitWeek DurationUnit = "WEEK" DurationUnitMonth DurationUnit = "MONTH" DurationUnitYear DurationUnit = "YEAR" ) type SubscriptionStatus string const ( SubscriptionStatusPending SubscriptionStatus = "PENDING" SubscriptionStatusActive SubscriptionStatus = "ACTIVE" SubscriptionStatusExpired SubscriptionStatus = "EXPIRED" SubscriptionStatusCancelled SubscriptionStatus = "CANCELLED" ) type SubscriptionPlan struct { ID int64 Name string Description *string Category string DurationValue int32 DurationUnit string Price float64 Currency string IsActive bool CreatedAt time.Time UpdatedAt *time.Time } type UserSubscription struct { ID int64 UserID int64 PlanID int64 StartsAt time.Time ExpiresAt time.Time Status string PaymentReference *string PaymentMethod *string AutoRenew bool CancelledAt *time.Time CreatedAt time.Time UpdatedAt *time.Time // Joined fields from plan PlanName *string DurationValue *int32 DurationUnit *string Price *float64 Currency *string } type CreateSubscriptionPlanInput struct { Name string Description *string Category string DurationValue int32 DurationUnit string Price float64 Currency string IsActive *bool } type UpdateSubscriptionPlanInput struct { Name *string Description *string Category *string DurationValue *int32 DurationUnit *string Price *float64 Currency *string IsActive *bool } type CreateUserSubscriptionInput struct { UserID int64 PlanID int64 StartsAt *time.Time ExpiresAt time.Time Status *string PaymentReference *string PaymentMethod *string AutoRenew *bool } // LifetimeSubscriptionExpiresAt is stored for one-time (non-expiring) DUOLINGO subscriptions. var LifetimeSubscriptionExpiresAt = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) // IsLifetimeSubscriptionCategory reports categories that grant permanent access after purchase. func IsLifetimeSubscriptionCategory(category string) bool { return strings.ToUpper(strings.TrimSpace(category)) == string(SubscriptionCategoryDuolingo) } // CalculateSubscriptionExpiresAt returns when a subscription ends. DUOLINGO plans never expire. func CalculateSubscriptionExpiresAt(startTime time.Time, category string, durationValue int32, durationUnit string) time.Time { if IsLifetimeSubscriptionCategory(category) { return LifetimeSubscriptionExpiresAt } return CalculateExpiryDate(startTime, durationValue, durationUnit) } // CalculateExpiryDate calculates the expiry date based on plan duration func CalculateExpiryDate(startTime time.Time, durationValue int32, durationUnit string) time.Time { switch durationUnit { case string(DurationUnitDay): return startTime.AddDate(0, 0, int(durationValue)) case string(DurationUnitWeek): return startTime.AddDate(0, 0, int(durationValue)*7) case string(DurationUnitMonth): return startTime.AddDate(0, int(durationValue), 0) case string(DurationUnitYear): return startTime.AddDate(int(durationValue), 0, 0) default: return startTime.AddDate(0, int(durationValue), 0) // Default to months } }