package services import ( "errors" "time" "gorm.io/gorm" "github.com/treytartt/mycrib-api/internal/models" "github.com/treytartt/mycrib-api/internal/repositories" ) // Subscription-related errors var ( ErrSubscriptionNotFound = errors.New("subscription not found") ErrPropertiesLimitExceeded = errors.New("properties limit exceeded for your subscription tier") ErrTasksLimitExceeded = errors.New("tasks limit exceeded for your subscription tier") ErrContractorsLimitExceeded = errors.New("contractors limit exceeded for your subscription tier") ErrDocumentsLimitExceeded = errors.New("documents limit exceeded for your subscription tier") ErrUpgradeTriggerNotFound = errors.New("upgrade trigger not found") ErrPromotionNotFound = errors.New("promotion not found") ) // SubscriptionService handles subscription business logic type SubscriptionService struct { subscriptionRepo *repositories.SubscriptionRepository residenceRepo *repositories.ResidenceRepository taskRepo *repositories.TaskRepository contractorRepo *repositories.ContractorRepository documentRepo *repositories.DocumentRepository } // NewSubscriptionService creates a new subscription service func NewSubscriptionService( subscriptionRepo *repositories.SubscriptionRepository, residenceRepo *repositories.ResidenceRepository, taskRepo *repositories.TaskRepository, contractorRepo *repositories.ContractorRepository, documentRepo *repositories.DocumentRepository, ) *SubscriptionService { return &SubscriptionService{ subscriptionRepo: subscriptionRepo, residenceRepo: residenceRepo, taskRepo: taskRepo, contractorRepo: contractorRepo, documentRepo: documentRepo, } } // GetSubscription gets the subscription for a user func (s *SubscriptionService) GetSubscription(userID uint) (*SubscriptionResponse, error) { sub, err := s.subscriptionRepo.GetOrCreate(userID) if err != nil { return nil, err } return NewSubscriptionResponse(sub), nil } // GetSubscriptionStatus gets detailed subscription status including limits func (s *SubscriptionService) GetSubscriptionStatus(userID uint) (*SubscriptionStatusResponse, error) { sub, err := s.subscriptionRepo.GetOrCreate(userID) if err != nil { return nil, err } settings, err := s.subscriptionRepo.GetSettings() if err != nil { return nil, err } limits, err := s.subscriptionRepo.GetTierLimits(sub.Tier) if err != nil { return nil, err } // Get current usage if limitations are enabled var usage *UsageResponse if settings.EnableLimitations { usage, err = s.getUserUsage(userID) if err != nil { return nil, err } } return &SubscriptionStatusResponse{ Subscription: NewSubscriptionResponse(sub), Limits: NewTierLimitsResponse(limits), Usage: usage, LimitationsEnabled: settings.EnableLimitations, }, nil } // getUserUsage calculates current usage for a user func (s *SubscriptionService) getUserUsage(userID uint) (*UsageResponse, error) { residences, err := s.residenceRepo.FindOwnedByUser(userID) if err != nil { return nil, err } propertiesCount := int64(len(residences)) // Count tasks, contractors, and documents across all user's residences var tasksCount, contractorsCount, documentsCount int64 for _, r := range residences { tc, err := s.taskRepo.CountByResidence(r.ID) if err != nil { return nil, err } tasksCount += tc cc, err := s.contractorRepo.CountByResidence(r.ID) if err != nil { return nil, err } contractorsCount += cc dc, err := s.documentRepo.CountByResidence(r.ID) if err != nil { return nil, err } documentsCount += dc } return &UsageResponse{ Properties: propertiesCount, Tasks: tasksCount, Contractors: contractorsCount, Documents: documentsCount, }, nil } // CheckLimit checks if a user has exceeded a specific limit func (s *SubscriptionService) CheckLimit(userID uint, limitType string) error { settings, err := s.subscriptionRepo.GetSettings() if err != nil { return err } // If limitations are disabled, allow everything if !settings.EnableLimitations { return nil } sub, err := s.subscriptionRepo.GetOrCreate(userID) if err != nil { return err } // Pro users have unlimited access if sub.IsPro() { return nil } limits, err := s.subscriptionRepo.GetTierLimits(sub.Tier) if err != nil { return err } usage, err := s.getUserUsage(userID) if err != nil { return err } switch limitType { case "properties": if limits.PropertiesLimit != nil && usage.Properties >= int64(*limits.PropertiesLimit) { return ErrPropertiesLimitExceeded } case "tasks": if limits.TasksLimit != nil && usage.Tasks >= int64(*limits.TasksLimit) { return ErrTasksLimitExceeded } case "contractors": if limits.ContractorsLimit != nil && usage.Contractors >= int64(*limits.ContractorsLimit) { return ErrContractorsLimitExceeded } case "documents": if limits.DocumentsLimit != nil && usage.Documents >= int64(*limits.DocumentsLimit) { return ErrDocumentsLimitExceeded } } return nil } // GetUpgradeTrigger gets an upgrade trigger by key func (s *SubscriptionService) GetUpgradeTrigger(key string) (*UpgradeTriggerResponse, error) { trigger, err := s.subscriptionRepo.GetUpgradeTrigger(key) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrUpgradeTriggerNotFound } return nil, err } return NewUpgradeTriggerResponse(trigger), nil } // GetFeatureBenefits gets all feature benefits func (s *SubscriptionService) GetFeatureBenefits() ([]FeatureBenefitResponse, error) { benefits, err := s.subscriptionRepo.GetFeatureBenefits() if err != nil { return nil, err } result := make([]FeatureBenefitResponse, len(benefits)) for i, b := range benefits { result[i] = *NewFeatureBenefitResponse(&b) } return result, nil } // GetActivePromotions gets active promotions for a user func (s *SubscriptionService) GetActivePromotions(userID uint) ([]PromotionResponse, error) { sub, err := s.subscriptionRepo.GetOrCreate(userID) if err != nil { return nil, err } promotions, err := s.subscriptionRepo.GetActivePromotions(sub.Tier) if err != nil { return nil, err } result := make([]PromotionResponse, len(promotions)) for i, p := range promotions { result[i] = *NewPromotionResponse(&p) } return result, nil } // ProcessApplePurchase processes an Apple IAP purchase func (s *SubscriptionService) ProcessApplePurchase(userID uint, receiptData string) (*SubscriptionResponse, error) { // TODO: Implement receipt validation with Apple's servers // For now, just upgrade the user // Store receipt data if err := s.subscriptionRepo.UpdateReceiptData(userID, receiptData); err != nil { return nil, err } // Upgrade to Pro (1 year from now - adjust based on actual subscription) expiresAt := time.Now().UTC().AddDate(1, 0, 0) if err := s.subscriptionRepo.UpgradeToPro(userID, expiresAt, "ios"); err != nil { return nil, err } return s.GetSubscription(userID) } // ProcessGooglePurchase processes a Google Play purchase func (s *SubscriptionService) ProcessGooglePurchase(userID uint, purchaseToken string) (*SubscriptionResponse, error) { // TODO: Implement token validation with Google's servers // For now, just upgrade the user // Store purchase token if err := s.subscriptionRepo.UpdatePurchaseToken(userID, purchaseToken); err != nil { return nil, err } // Upgrade to Pro (1 year from now - adjust based on actual subscription) expiresAt := time.Now().UTC().AddDate(1, 0, 0) if err := s.subscriptionRepo.UpgradeToPro(userID, expiresAt, "android"); err != nil { return nil, err } return s.GetSubscription(userID) } // CancelSubscription cancels a subscription (downgrades to free at end of period) func (s *SubscriptionService) CancelSubscription(userID uint) (*SubscriptionResponse, error) { if err := s.subscriptionRepo.SetAutoRenew(userID, false); err != nil { return nil, err } return s.GetSubscription(userID) } // === Response Types === // SubscriptionResponse represents a subscription in API response type SubscriptionResponse struct { Tier string `json:"tier"` SubscribedAt *string `json:"subscribed_at"` ExpiresAt *string `json:"expires_at"` AutoRenew bool `json:"auto_renew"` CancelledAt *string `json:"cancelled_at"` Platform string `json:"platform"` IsActive bool `json:"is_active"` IsPro bool `json:"is_pro"` } // NewSubscriptionResponse creates a SubscriptionResponse from a model func NewSubscriptionResponse(s *models.UserSubscription) *SubscriptionResponse { resp := &SubscriptionResponse{ Tier: string(s.Tier), AutoRenew: s.AutoRenew, Platform: s.Platform, IsActive: s.IsActive(), IsPro: s.IsPro(), } if s.SubscribedAt != nil { t := s.SubscribedAt.Format("2006-01-02T15:04:05Z") resp.SubscribedAt = &t } if s.ExpiresAt != nil { t := s.ExpiresAt.Format("2006-01-02T15:04:05Z") resp.ExpiresAt = &t } if s.CancelledAt != nil { t := s.CancelledAt.Format("2006-01-02T15:04:05Z") resp.CancelledAt = &t } return resp } // TierLimitsResponse represents tier limits type TierLimitsResponse struct { Tier string `json:"tier"` PropertiesLimit *int `json:"properties_limit"` TasksLimit *int `json:"tasks_limit"` ContractorsLimit *int `json:"contractors_limit"` DocumentsLimit *int `json:"documents_limit"` } // NewTierLimitsResponse creates a TierLimitsResponse from a model func NewTierLimitsResponse(l *models.TierLimits) *TierLimitsResponse { return &TierLimitsResponse{ Tier: string(l.Tier), PropertiesLimit: l.PropertiesLimit, TasksLimit: l.TasksLimit, ContractorsLimit: l.ContractorsLimit, DocumentsLimit: l.DocumentsLimit, } } // UsageResponse represents current usage type UsageResponse struct { Properties int64 `json:"properties"` Tasks int64 `json:"tasks"` Contractors int64 `json:"contractors"` Documents int64 `json:"documents"` } // SubscriptionStatusResponse represents full subscription status type SubscriptionStatusResponse struct { Subscription *SubscriptionResponse `json:"subscription"` Limits *TierLimitsResponse `json:"limits"` Usage *UsageResponse `json:"usage,omitempty"` LimitationsEnabled bool `json:"limitations_enabled"` } // UpgradeTriggerResponse represents an upgrade trigger type UpgradeTriggerResponse struct { TriggerKey string `json:"trigger_key"` Title string `json:"title"` Message string `json:"message"` PromoHTML string `json:"promo_html"` ButtonText string `json:"button_text"` } // NewUpgradeTriggerResponse creates an UpgradeTriggerResponse from a model func NewUpgradeTriggerResponse(t *models.UpgradeTrigger) *UpgradeTriggerResponse { return &UpgradeTriggerResponse{ TriggerKey: t.TriggerKey, Title: t.Title, Message: t.Message, PromoHTML: t.PromoHTML, ButtonText: t.ButtonText, } } // FeatureBenefitResponse represents a feature benefit type FeatureBenefitResponse struct { FeatureName string `json:"feature_name"` FreeTierText string `json:"free_tier_text"` ProTierText string `json:"pro_tier_text"` DisplayOrder int `json:"display_order"` } // NewFeatureBenefitResponse creates a FeatureBenefitResponse from a model func NewFeatureBenefitResponse(f *models.FeatureBenefit) *FeatureBenefitResponse { return &FeatureBenefitResponse{ FeatureName: f.FeatureName, FreeTierText: f.FreeTierText, ProTierText: f.ProTierText, DisplayOrder: f.DisplayOrder, } } // PromotionResponse represents a promotion type PromotionResponse struct { PromotionID string `json:"promotion_id"` Title string `json:"title"` Message string `json:"message"` Link *string `json:"link"` StartDate string `json:"start_date"` EndDate string `json:"end_date"` } // NewPromotionResponse creates a PromotionResponse from a model func NewPromotionResponse(p *models.Promotion) *PromotionResponse { return &PromotionResponse{ PromotionID: p.PromotionID, Title: p.Title, Message: p.Message, Link: p.Link, StartDate: p.StartDate.Format("2006-01-02"), EndDate: p.EndDate.Format("2006-01-02"), } } // === Request Types === // ProcessPurchaseRequest represents an IAP purchase request type ProcessPurchaseRequest struct { ReceiptData string `json:"receipt_data"` // iOS PurchaseToken string `json:"purchase_token"` // Android Platform string `json:"platform" binding:"required,oneof=ios android"` }