- Stripe integration: add StripeService with checkout sessions, customer portal, and webhook handling for subscription lifecycle events. - Free trials: auto-start configurable trial on first subscription check, with admin-controllable duration and enable/disable toggle. - Cross-platform guard: prevent duplicate subscriptions across iOS, Android, and Stripe by checking existing platform before allowing purchase. - Subscription model: add Stripe fields (customer_id, subscription_id, price_id), trial fields (trial_start, trial_end, trial_used), and SubscriptionSource/IsTrialActive helpers. - API: add trial and source fields to status response, update OpenAPI spec. - Clean up stale migration and audit docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
219 lines
8.1 KiB
Go
219 lines
8.1 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
)
|
|
|
|
// SubscriptionTier represents the subscription tier
|
|
type SubscriptionTier string
|
|
|
|
const (
|
|
TierFree SubscriptionTier = "free"
|
|
TierPro SubscriptionTier = "pro"
|
|
)
|
|
|
|
// SubscriptionPlatform constants
|
|
const (
|
|
PlatformIOS = "ios"
|
|
PlatformAndroid = "android"
|
|
PlatformStripe = "stripe"
|
|
)
|
|
|
|
// SubscriptionSettings represents the subscription_subscriptionsettings table (singleton)
|
|
type SubscriptionSettings struct {
|
|
ID uint `gorm:"primaryKey" json:"id"`
|
|
EnableLimitations bool `gorm:"column:enable_limitations;default:false" json:"enable_limitations"`
|
|
EnableMonitoring bool `gorm:"column:enable_monitoring;default:true" json:"enable_monitoring"`
|
|
TrialEnabled bool `gorm:"column:trial_enabled;default:true" json:"trial_enabled"`
|
|
TrialDurationDays int `gorm:"column:trial_duration_days;default:14" json:"trial_duration_days"`
|
|
}
|
|
|
|
// TableName returns the table name for GORM
|
|
func (SubscriptionSettings) TableName() string {
|
|
return "subscription_subscriptionsettings"
|
|
}
|
|
|
|
// UserSubscription represents the subscription_usersubscription table
|
|
type UserSubscription struct {
|
|
BaseModel
|
|
UserID uint `gorm:"column:user_id;uniqueIndex;not null" json:"user_id"`
|
|
User User `gorm:"foreignKey:UserID" json:"-"`
|
|
Tier SubscriptionTier `gorm:"column:tier;size:10;default:'free'" json:"tier"`
|
|
|
|
// In-App Purchase data (Apple / Google)
|
|
AppleReceiptData *string `gorm:"column:apple_receipt_data;type:text" json:"-"`
|
|
GooglePurchaseToken *string `gorm:"column:google_purchase_token;type:text" json:"-"`
|
|
|
|
// Stripe data (web subscriptions)
|
|
StripeCustomerID *string `gorm:"column:stripe_customer_id;size:255" json:"-"`
|
|
StripeSubscriptionID *string `gorm:"column:stripe_subscription_id;size:255" json:"-"`
|
|
StripePriceID *string `gorm:"column:stripe_price_id;size:255" json:"-"`
|
|
|
|
// Subscription dates
|
|
SubscribedAt *time.Time `gorm:"column:subscribed_at" json:"subscribed_at"`
|
|
ExpiresAt *time.Time `gorm:"column:expires_at" json:"expires_at"`
|
|
AutoRenew bool `gorm:"column:auto_renew;default:true" json:"auto_renew"`
|
|
|
|
// Trial
|
|
TrialStart *time.Time `gorm:"column:trial_start" json:"trial_start"`
|
|
TrialEnd *time.Time `gorm:"column:trial_end" json:"trial_end"`
|
|
TrialUsed bool `gorm:"column:trial_used;default:false" json:"trial_used"`
|
|
|
|
// Tracking
|
|
CancelledAt *time.Time `gorm:"column:cancelled_at" json:"cancelled_at"`
|
|
Platform string `gorm:"column:platform;size:10" json:"platform"` // ios, android, stripe
|
|
|
|
// Admin override - bypasses all limitations regardless of global settings
|
|
IsFree bool `gorm:"column:is_free;default:false" json:"is_free"`
|
|
}
|
|
|
|
// TableName returns the table name for GORM
|
|
func (UserSubscription) TableName() string {
|
|
return "subscription_usersubscription"
|
|
}
|
|
|
|
// IsActive returns true if the subscription is active (pro tier and not expired, or trial active)
|
|
func (s *UserSubscription) IsActive() bool {
|
|
if s.IsTrialActive() {
|
|
return true
|
|
}
|
|
if s.Tier != TierPro {
|
|
return false
|
|
}
|
|
if s.ExpiresAt != nil && time.Now().UTC().After(*s.ExpiresAt) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IsPro returns true if the user has a pro subscription or active trial
|
|
func (s *UserSubscription) IsPro() bool {
|
|
return s.IsActive()
|
|
}
|
|
|
|
// IsTrialActive returns true if the user has an active, unexpired trial
|
|
func (s *UserSubscription) IsTrialActive() bool {
|
|
if s.TrialEnd == nil {
|
|
return false
|
|
}
|
|
return time.Now().UTC().Before(*s.TrialEnd)
|
|
}
|
|
|
|
// HasStripeSubscription returns true if the user has Stripe subscription data
|
|
func (s *UserSubscription) HasStripeSubscription() bool {
|
|
return s.StripeSubscriptionID != nil && *s.StripeSubscriptionID != ""
|
|
}
|
|
|
|
// HasAppleSubscription returns true if the user has Apple receipt data
|
|
func (s *UserSubscription) HasAppleSubscription() bool {
|
|
return s.AppleReceiptData != nil && *s.AppleReceiptData != ""
|
|
}
|
|
|
|
// HasGoogleSubscription returns true if the user has Google purchase token
|
|
func (s *UserSubscription) HasGoogleSubscription() bool {
|
|
return s.GooglePurchaseToken != nil && *s.GooglePurchaseToken != ""
|
|
}
|
|
|
|
// SubscriptionSource returns the platform that the active subscription came from
|
|
func (s *UserSubscription) SubscriptionSource() string {
|
|
return s.Platform
|
|
}
|
|
|
|
// UpgradeTrigger represents the subscription_upgradetrigger table
|
|
type UpgradeTrigger struct {
|
|
BaseModel
|
|
TriggerKey string `gorm:"column:trigger_key;uniqueIndex;size:50;not null" json:"trigger_key"`
|
|
Title string `gorm:"column:title;size:200;not null" json:"title"`
|
|
Message string `gorm:"column:message;type:text;not null" json:"message"`
|
|
PromoHTML string `gorm:"column:promo_html;type:text" json:"promo_html"`
|
|
ButtonText string `gorm:"column:button_text;size:50;default:'Upgrade to Pro'" json:"button_text"`
|
|
IsActive bool `gorm:"column:is_active;default:true" json:"is_active"`
|
|
}
|
|
|
|
// TableName returns the table name for GORM
|
|
func (UpgradeTrigger) TableName() string {
|
|
return "subscription_upgradetrigger"
|
|
}
|
|
|
|
// FeatureBenefit represents the subscription_featurebenefit table
|
|
type FeatureBenefit struct {
|
|
BaseModel
|
|
FeatureName string `gorm:"column:feature_name;size:200;not null" json:"feature_name"`
|
|
FreeTierText string `gorm:"column:free_tier_text;size:200;not null" json:"free_tier_text"`
|
|
ProTierText string `gorm:"column:pro_tier_text;size:200;not null" json:"pro_tier_text"`
|
|
DisplayOrder int `gorm:"column:display_order;default:0" json:"display_order"`
|
|
IsActive bool `gorm:"column:is_active;default:true" json:"is_active"`
|
|
}
|
|
|
|
// TableName returns the table name for GORM
|
|
func (FeatureBenefit) TableName() string {
|
|
return "subscription_featurebenefit"
|
|
}
|
|
|
|
// Promotion represents the subscription_promotion table
|
|
type Promotion struct {
|
|
BaseModel
|
|
PromotionID string `gorm:"column:promotion_id;uniqueIndex;size:50;not null" json:"promotion_id"`
|
|
Title string `gorm:"column:title;size:200;not null" json:"title"`
|
|
Message string `gorm:"column:message;type:text;not null" json:"message"`
|
|
Link *string `gorm:"column:link;size:200" json:"link"`
|
|
StartDate time.Time `gorm:"column:start_date;not null" json:"start_date"`
|
|
EndDate time.Time `gorm:"column:end_date;not null" json:"end_date"`
|
|
TargetTier SubscriptionTier `gorm:"column:target_tier;size:10;default:'free'" json:"target_tier"`
|
|
IsActive bool `gorm:"column:is_active;default:true" json:"is_active"`
|
|
}
|
|
|
|
// TableName returns the table name for GORM
|
|
func (Promotion) TableName() string {
|
|
return "subscription_promotion"
|
|
}
|
|
|
|
// IsCurrentlyActive returns true if the promotion is currently active
|
|
func (p *Promotion) IsCurrentlyActive() bool {
|
|
if !p.IsActive {
|
|
return false
|
|
}
|
|
now := time.Now().UTC()
|
|
return now.After(p.StartDate) && now.Before(p.EndDate)
|
|
}
|
|
|
|
// TierLimits represents the subscription_tierlimits table
|
|
type TierLimits struct {
|
|
BaseModel
|
|
Tier SubscriptionTier `gorm:"column:tier;uniqueIndex;size:10;not null" json:"tier"`
|
|
PropertiesLimit *int `gorm:"column:properties_limit" json:"properties_limit"`
|
|
TasksLimit *int `gorm:"column:tasks_limit" json:"tasks_limit"`
|
|
ContractorsLimit *int `gorm:"column:contractors_limit" json:"contractors_limit"`
|
|
DocumentsLimit *int `gorm:"column:documents_limit" json:"documents_limit"`
|
|
}
|
|
|
|
// TableName returns the table name for GORM
|
|
func (TierLimits) TableName() string {
|
|
return "subscription_tierlimits"
|
|
}
|
|
|
|
// GetDefaultFreeLimits returns the default limits for the free tier
|
|
func GetDefaultFreeLimits() TierLimits {
|
|
one := 1
|
|
ten := 10
|
|
zero := 0
|
|
return TierLimits{
|
|
Tier: TierFree,
|
|
PropertiesLimit: &one,
|
|
TasksLimit: &ten,
|
|
ContractorsLimit: &zero,
|
|
DocumentsLimit: &zero,
|
|
}
|
|
}
|
|
|
|
// GetDefaultProLimits returns the default limits for the pro tier (unlimited)
|
|
func GetDefaultProLimits() TierLimits {
|
|
return TierLimits{
|
|
Tier: TierPro,
|
|
PropertiesLimit: nil, // nil = unlimited
|
|
TasksLimit: nil,
|
|
ContractorsLimit: nil,
|
|
DocumentsLimit: nil,
|
|
}
|
|
}
|