- Create CreatePlanModal component with Headless UI Dialog - Implement dark mode support throughout plans UI - Update plans/page.tsx with professional card layout - Update plans/[id]/page.tsx with consistent styling - Add proper spacing, typography, and color consistency - Implement smooth animations and transitions - Add success/error message feedback - Improve form UX with better input styling
287 lines
7.2 KiB
Go
287 lines
7.2 KiB
Go
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)
|
|
}
|