Yimaru-BackEnd/internal/domain/subscriptions.go
Yared Yemane 4f873fe9de chore: temporarily disable exam-prep subscription enforcement
Add ExamPrepSubscriptionGateDisabled so learners can access exam-prep content without an IELTS or Duolingo plan. LMS subscription gating is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 04:44:18 -07:00

188 lines
5.6 KiB
Go

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
}
}