LEARN_ENGLISH plans unlock LMS only; IELTS and DUOLINGO unlock matching exam-prep catalog courses. Enable category subscription gating, restrict programs to Learn English, and treat Duolingo subscriptions as non-expiring one-time purchases. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
5.5 KiB
Go
185 lines
5.5 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
|
|
|
|
// 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
|
|
}
|
|
}
|