mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
284 lines
8.5 KiB
Go
284 lines
8.5 KiB
Go
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]
|
|
}
|