package service import ( "database/sql" "errors" "fmt" "aggios-app/backend/internal/domain" "aggios-app/backend/internal/repository" "github.com/google/uuid" "github.com/shopspring/decimal" ) var ( ErrPlanNotFound = errors.New("plan not found") ErrPlanSlugTaken = errors.New("plan slug already exists") ErrInvalidUserRange = errors.New("invalid user range: min_users must be less than or equal to max_users") ErrSubscriptionNotFound = errors.New("subscription not found") ErrUserLimitExceeded = errors.New("user limit exceeded for this plan") ErrSubscriptionExists = errors.New("agency already has an active subscription") ) // PlanService handles plan business logic type PlanService struct { planRepo *repository.PlanRepository subscriptionRepo *repository.SubscriptionRepository } // NewPlanService creates a new plan service func NewPlanService(planRepo *repository.PlanRepository, subscriptionRepo *repository.SubscriptionRepository) *PlanService { return &PlanService{ planRepo: planRepo, subscriptionRepo: subscriptionRepo, } } // CreatePlan creates a new plan func (s *PlanService) CreatePlan(req *domain.CreatePlanRequest) (*domain.Plan, error) { // Validate user range if req.MinUsers > req.MaxUsers && req.MaxUsers != -1 { return nil, ErrInvalidUserRange } // Check if slug is unique existing, _ := s.planRepo.GetBySlug(req.Slug) if existing != nil { return nil, ErrPlanSlugTaken } plan := &domain.Plan{ Name: req.Name, Slug: req.Slug, Description: req.Description, MinUsers: req.MinUsers, MaxUsers: req.MaxUsers, Features: req.Features, Differentiators: req.Differentiators, StorageGB: req.StorageGB, IsActive: req.IsActive, } // Convert prices if provided if req.MonthlyPrice != nil { price := decimal.NewFromFloat(*req.MonthlyPrice) plan.MonthlyPrice = &price } if req.AnnualPrice != nil { price := decimal.NewFromFloat(*req.AnnualPrice) plan.AnnualPrice = &price } if err := s.planRepo.Create(plan); err != nil { return nil, err } return plan, nil } // GetPlan retrieves a plan by ID func (s *PlanService) GetPlan(id uuid.UUID) (*domain.Plan, error) { plan, err := s.planRepo.GetByID(id) if err != nil { if err == sql.ErrNoRows { return nil, ErrPlanNotFound } return nil, err } return plan, nil } // ListPlans retrieves all plans func (s *PlanService) ListPlans() ([]*domain.Plan, error) { return s.planRepo.ListAll() } // ListActivePlans retrieves all active plans func (s *PlanService) ListActivePlans() ([]*domain.Plan, error) { return s.planRepo.ListActive() } // UpdatePlan updates a plan func (s *PlanService) UpdatePlan(id uuid.UUID, req *domain.UpdatePlanRequest) (*domain.Plan, error) { plan, err := s.planRepo.GetByID(id) if err != nil { if err == sql.ErrNoRows { return nil, ErrPlanNotFound } return nil, err } // Update fields if provided if req.Name != nil { plan.Name = *req.Name } if req.Slug != nil { // Check if new slug is unique existing, _ := s.planRepo.GetBySlug(*req.Slug) if existing != nil && existing.ID != plan.ID { return nil, ErrPlanSlugTaken } plan.Slug = *req.Slug } if req.Description != nil { plan.Description = *req.Description } if req.MinUsers != nil { plan.MinUsers = *req.MinUsers } if req.MaxUsers != nil { plan.MaxUsers = *req.MaxUsers } if req.MonthlyPrice != nil { price := decimal.NewFromFloat(*req.MonthlyPrice) plan.MonthlyPrice = &price } if req.AnnualPrice != nil { price := decimal.NewFromFloat(*req.AnnualPrice) plan.AnnualPrice = &price } if req.Features != nil { plan.Features = req.Features } if req.Differentiators != nil { plan.Differentiators = req.Differentiators } if req.StorageGB != nil { plan.StorageGB = *req.StorageGB } if req.IsActive != nil { plan.IsActive = *req.IsActive } // Validate user range if plan.MinUsers > plan.MaxUsers && plan.MaxUsers != -1 { return nil, ErrInvalidUserRange } if err := s.planRepo.Update(plan); err != nil { return nil, err } return plan, nil } // DeletePlan deletes a plan func (s *PlanService) DeletePlan(id uuid.UUID) error { // Check if plan exists if _, err := s.planRepo.GetByID(id); err != nil { if err == sql.ErrNoRows { return ErrPlanNotFound } return err } return s.planRepo.Delete(id) } // CreateSubscription creates a new subscription for an agency func (s *PlanService) CreateSubscription(req *domain.CreateSubscriptionRequest) (*domain.Subscription, error) { // Check if plan exists plan, err := s.planRepo.GetByID(req.PlanID) if err != nil { if err == sql.ErrNoRows { return nil, ErrPlanNotFound } return nil, err } // Check if agency already has active subscription existing, err := s.subscriptionRepo.GetByAgencyID(req.AgencyID) if err != nil && err != sql.ErrNoRows { return nil, err } if existing != nil { return nil, ErrSubscriptionExists } subscription := &domain.Subscription{ AgencyID: req.AgencyID, PlanID: req.PlanID, BillingType: req.BillingType, Status: "active", CurrentUsers: 0, } if err := s.subscriptionRepo.Create(subscription); err != nil { return nil, err } // Load plan details subscription.PlanID = plan.ID return subscription, nil } // GetSubscription retrieves a subscription by ID func (s *PlanService) GetSubscription(id uuid.UUID) (*domain.Subscription, error) { subscription, err := s.subscriptionRepo.GetByID(id) if err != nil { if err == sql.ErrNoRows { return nil, ErrSubscriptionNotFound } return nil, err } return subscription, nil } // GetAgencySubscription retrieves an agency's active subscription func (s *PlanService) GetAgencySubscription(agencyID uuid.UUID) (*domain.Subscription, error) { subscription, err := s.subscriptionRepo.GetByAgencyID(agencyID) if err != nil { if err == sql.ErrNoRows { return nil, ErrSubscriptionNotFound } return nil, err } return subscription, nil } // ListSubscriptions retrieves all subscriptions func (s *PlanService) ListSubscriptions() ([]*domain.Subscription, error) { return s.subscriptionRepo.ListAll() } // ValidateUserLimit checks if adding a user would exceed plan limit func (s *PlanService) ValidateUserLimit(agencyID uuid.UUID, newUserCount int) error { subscription, err := s.subscriptionRepo.GetByAgencyID(agencyID) if err != nil { if err == sql.ErrNoRows { return ErrSubscriptionNotFound } return err } plan, err := s.planRepo.GetByID(subscription.PlanID) if err != nil { if err == sql.ErrNoRows { return ErrPlanNotFound } return err } if plan.MaxUsers != -1 && newUserCount > plan.MaxUsers { return fmt.Errorf("%w (limit: %d, requested: %d)", ErrUserLimitExceeded, plan.MaxUsers, newUserCount) } return nil } // GetPlanByUserCount returns the appropriate plan for a given user count func (s *PlanService) GetPlanByUserCount(userCount int) (*domain.Plan, error) { plans, err := s.planRepo.ListActive() if err != nil { return nil, err } // Find the plan that fits the user count for _, plan := range plans { if userCount >= plan.MinUsers && (plan.MaxUsers == -1 || userCount <= plan.MaxUsers) { return plan, nil } } return nil, fmt.Errorf("no plan found for user count: %d", userCount) }