package service import ( "context" "errors" "fmt" "time" "bugulma/backend/internal/domain" "bugulma/backend/internal/repository" "github.com/google/uuid" ) // SubscriptionFeature represents a subscription feature flag type SubscriptionFeature string const ( FeatureUnlimitedOrganizations SubscriptionFeature = "unlimited_organizations" FeatureAdvancedAnalytics SubscriptionFeature = "advanced_analytics" FeatureAPIAccess SubscriptionFeature = "api_access" FeatureCustomDomain SubscriptionFeature = "custom_domain" FeatureSSO SubscriptionFeature = "sso" FeaturePrioritySupport SubscriptionFeature = "priority_support" FeatureDedicatedSupport SubscriptionFeature = "dedicated_support" FeatureTeamCollaboration SubscriptionFeature = "team_collaboration" FeatureWhiteLabel SubscriptionFeature = "white_label" ) // PlanFeatures maps plans to their features var PlanFeatures = map[domain.SubscriptionPlan][]SubscriptionFeature{ domain.SubscriptionPlanFree: {}, domain.SubscriptionPlanBasic: {FeatureTeamCollaboration}, domain.SubscriptionPlanProfessional: { FeatureUnlimitedOrganizations, FeatureAdvancedAnalytics, FeatureAPIAccess, FeatureTeamCollaboration, FeaturePrioritySupport, }, domain.SubscriptionPlanEnterprise: { FeatureUnlimitedOrganizations, FeatureAdvancedAnalytics, FeatureAPIAccess, FeatureCustomDomain, FeatureSSO, FeatureTeamCollaboration, FeatureDedicatedSupport, FeatureWhiteLabel, }, } // PlanLimits maps plans to their limits (-1 means unlimited) var PlanLimits = map[domain.SubscriptionPlan]map[domain.UsageLimitType]int64{ domain.SubscriptionPlanFree: { domain.UsageLimitTypeOrganizations: 3, domain.UsageLimitTypeUsers: 1, domain.UsageLimitTypeStorage: 100, // MB domain.UsageLimitTypeAPICalls: 1000, }, domain.SubscriptionPlanBasic: { domain.UsageLimitTypeOrganizations: 10, domain.UsageLimitTypeUsers: 5, domain.UsageLimitTypeStorage: 1000, // MB domain.UsageLimitTypeAPICalls: 10000, }, domain.SubscriptionPlanProfessional: { domain.UsageLimitTypeOrganizations: -1, // unlimited domain.UsageLimitTypeUsers: 20, domain.UsageLimitTypeStorage: 10000, // MB domain.UsageLimitTypeAPICalls: 100000, }, domain.SubscriptionPlanEnterprise: { domain.UsageLimitTypeOrganizations: -1, // unlimited domain.UsageLimitTypeUsers: -1, // unlimited domain.UsageLimitTypeStorage: -1, // unlimited domain.UsageLimitTypeAPICalls: -1, // unlimited domain.UsageLimitTypeCustomDomains: 5, }, } type SubscriptionService struct { subscriptionRepo domain.SubscriptionRepository usageRepo domain.UsageTrackingRepository userRepo domain.UserRepository } func NewSubscriptionService( subscriptionRepo domain.SubscriptionRepository, usageRepo domain.UsageTrackingRepository, userRepo domain.UserRepository, ) *SubscriptionService { return &SubscriptionService{ subscriptionRepo: subscriptionRepo, usageRepo: usageRepo, userRepo: userRepo, } } // GetSubscription retrieves a user's subscription, creating a free one if none exists func (s *SubscriptionService) GetSubscription(ctx context.Context, userID string) (*domain.Subscription, error) { subscription, err := s.subscriptionRepo.GetByUserID(ctx, userID) if err != nil { if errors.Is(err, repository.ErrNotFound) { // Create default free subscription return s.CreateSubscription(ctx, userID, domain.SubscriptionPlanFree, domain.BillingPeriodMonthly) } return nil, err } return subscription, nil } // CreateSubscription creates a new subscription func (s *SubscriptionService) CreateSubscription(ctx context.Context, userID string, plan domain.SubscriptionPlan, billingPeriod domain.BillingPeriod) (*domain.Subscription, error) { // Verify user exists _, err := s.userRepo.GetByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } now := time.Now() var periodEnd time.Time if billingPeriod == domain.BillingPeriodYearly { periodEnd = now.AddDate(1, 0, 0) } else { periodEnd = now.AddDate(0, 1, 0) } subscription := &domain.Subscription{ ID: uuid.New().String(), UserID: userID, Plan: plan, Status: domain.SubscriptionStatusActive, BillingPeriod: billingPeriod, CurrentPeriodStart: now, CurrentPeriodEnd: periodEnd, CancelAtPeriodEnd: false, } if plan == domain.SubscriptionPlanFree { subscription.Status = domain.SubscriptionStatusNone } if err := s.subscriptionRepo.Create(ctx, subscription); err != nil { return nil, fmt.Errorf("failed to create subscription: %w", err) } return subscription, nil } // UpdateSubscription updates a subscription (upgrade/downgrade) func (s *SubscriptionService) UpdateSubscription(ctx context.Context, subscriptionID string, newPlan domain.SubscriptionPlan) (*domain.Subscription, error) { subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID) if err != nil { return nil, err } subscription.Plan = newPlan if newPlan == domain.SubscriptionPlanFree { subscription.Status = domain.SubscriptionStatusNone } else if subscription.Status == domain.SubscriptionStatusNone { subscription.Status = domain.SubscriptionStatusActive } if err := s.subscriptionRepo.Update(ctx, subscription); err != nil { return nil, fmt.Errorf("failed to update subscription: %w", err) } return subscription, nil } // CancelSubscription cancels a subscription func (s *SubscriptionService) CancelSubscription(ctx context.Context, subscriptionID string) error { subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID) if err != nil { return fmt.Errorf("subscription not found: %w", err) } subscription.CancelAtPeriodEnd = true if err := s.subscriptionRepo.Update(ctx, subscription); err != nil { return fmt.Errorf("failed to cancel subscription: %w", err) } return nil } // CheckFeatureAccess checks if a user has access to a specific feature func (s *SubscriptionService) CheckFeatureAccess(ctx context.Context, userID string, feature SubscriptionFeature) (bool, error) { subscription, err := s.GetSubscription(ctx, userID) if err != nil { return false, err } // Check if subscription is active if !s.isSubscriptionActive(subscription.Status) { return false, nil } // Check if plan has feature features, ok := PlanFeatures[subscription.Plan] if !ok { return false, nil } for _, f := range features { if f == feature { return true, nil } } return false, nil } // CheckLimits checks if a user is within their subscription limits func (s *SubscriptionService) CheckLimits(ctx context.Context, userID string, limitType domain.UsageLimitType, current int64) (bool, int64, error) { subscription, err := s.GetSubscription(ctx, userID) if err != nil { return false, 0, err } // Get plan limits planLimits, ok := PlanLimits[subscription.Plan] if !ok { return false, 0, fmt.Errorf("unknown plan: %s", subscription.Plan) } limit, ok := planLimits[limitType] if !ok { // No limit for this type return true, -1, nil } if limit == -1 { // Unlimited return true, -1, nil } return current < limit, limit - current, nil } // GetUsageStats retrieves usage statistics for a user func (s *SubscriptionService) GetUsageStats(ctx context.Context, userID string) (map[domain.UsageLimitType]int64, error) { subscription, err := s.GetSubscription(ctx, userID) if err != nil { return nil, err } stats := make(map[domain.UsageLimitType]int64) planLimits, ok := PlanLimits[subscription.Plan] if !ok { return stats, nil } // Get usage for each limit type for limitType := range planLimits { usage, err := s.usageRepo.GetCurrentPeriodUsage(ctx, userID, limitType) if err != nil { if errors.Is(err, repository.ErrNotFound) { stats[limitType] = 0 continue } return nil, err } stats[limitType] = usage.CurrentUsage } return stats, nil } // isSubscriptionActive checks if a subscription status is active func (s *SubscriptionService) isSubscriptionActive(status domain.SubscriptionStatus) bool { return status == domain.SubscriptionStatusActive || status == domain.SubscriptionStatusTrialing } // GetPlanFeatures returns features for a plan func (s *SubscriptionService) GetPlanFeatures(plan domain.SubscriptionPlan) []SubscriptionFeature { return PlanFeatures[plan] } // GetPlanLimits returns limits for a plan func (s *SubscriptionService) GetPlanLimits(plan domain.SubscriptionPlan) map[domain.UsageLimitType]int64 { return PlanLimits[plan] }