package service import ( "context" "fmt" "github.com/gochat/gochat/internal/campaign" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" pkgvalidator "github.com/gochat/gochat/pkg/validator" ) // CampaignService implements business logic for Campaign operations. // Reference: Chatwoot app/controllers/api/v1/campaigns_controller.rb type CampaignService struct { campaignSvc *campaign.CampaignService campaignRepo *repository.CampaignRepo } // NewCampaignService creates a new Campaign service. func NewCampaignService(campaignSvc *campaign.CampaignService, campaignRepo *repository.CampaignRepo) *CampaignService { return &CampaignService{campaignSvc: campaignSvc, campaignRepo: campaignRepo} } // CreateCampaignRequest is the DTO for creating a campaign. type CreateCampaignRequest struct { InboxID uint `json:"inbox_id" validate:"required"` SenderID *uint `json:"sender_id,omitempty"` Title string `json:"title" validate:"required,min=2"` Message string `json:"message" validate:"required"` Description string `json:"description,omitempty"` CampaignType string `json:"campaign_type" validate:"required,oneof=ongoing one_off"` Audience string `json:"audience,omitempty"` TriggerRules string `json:"trigger_rules,omitempty"` TemplateParams string `json:"template_params,omitempty"` ScheduledAt *string `json:"scheduled_at,omitempty"` // ISO8601 timestamp string Enabled *bool `json:"enabled,omitempty"` TriggerOnlyDuringBusinessHours *bool `json:"trigger_only_during_business_hours,omitempty"` } // UpdateCampaignRequest is the DTO for updating a campaign. type UpdateCampaignRequest struct { Title string `json:"title,omitempty" validate:"omitempty,min=2"` Message string `json:"message,omitempty"` Description string `json:"description,omitempty"` CampaignType string `json:"campaign_type,omitempty" validate:"omitempty,oneof=ongoing one_off"` Audience string `json:"audience,omitempty"` TriggerRules string `json:"trigger_rules,omitempty"` TemplateParams string `json:"template_params,omitempty"` ScheduledAt *string `json:"scheduled_at,omitempty"` Enabled *bool `json:"enabled,omitempty"` TriggerOnlyDuringBusinessHours *bool `json:"trigger_only_during_business_hours,omitempty"` } // List retrieves all campaigns for an account with pagination. func (s *CampaignService) List(ctx context.Context, accountID uint, offset, limit int) ([]campaign.Campaign, int64, error) { return s.campaignRepo.ListByAccount(ctx, accountID, offset, limit) } // Get retrieves a single campaign by ID scoped to an account. func (s *CampaignService) Get(ctx context.Context, id, accountID uint) (*campaign.Campaign, error) { c, err := s.campaignRepo.FindByIDAndAccount(ctx, id, accountID) if err != nil { return nil, fmt.Errorf("campaign not found: %w", err) } return c, nil } // Create creates a new campaign within an account. func (s *CampaignService) Create(ctx context.Context, accountID uint, req CreateCampaignRequest) (*campaign.Campaign, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } enabled := true if req.Enabled != nil { enabled = *req.Enabled } triggerDuringBH := false if req.TriggerOnlyDuringBusinessHours != nil { triggerDuringBH = *req.TriggerOnlyDuringBusinessHours } c := &campaign.Campaign{ AccountID: accountID, InboxID: req.InboxID, SenderID: req.SenderID, Title: req.Title, Message: req.Message, Description: req.Description, CampaignStatus: campaign.CampaignStatusActive, CampaignType: campaign.CampaignType(req.CampaignType), Audience: req.Audience, TriggerRules: req.TriggerRules, TemplateParams: req.TemplateParams, Enabled: enabled, TriggerOnlyDuringBusinessHours: triggerDuringBH, } // Parse scheduled_at if provided if req.ScheduledAt != nil && *req.ScheduledAt != "" { // ScheduledAt parsing deferred — stored as string for now // Full implementation would parse ISO8601 to time.Time } if err := s.campaignSvc.Create(ctx, c); err != nil { applogger.L().Errorf("failed to create campaign: %v", err) return nil, fmt.Errorf("failed to create campaign: %w", err) } return c, nil } // Update updates an existing campaign scoped to an account. func (s *CampaignService) Update(ctx context.Context, id, accountID uint, req UpdateCampaignRequest) (*campaign.Campaign, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, err } c, err := s.campaignRepo.FindByIDAndAccount(ctx, id, accountID) if err != nil { return nil, fmt.Errorf("campaign not found: %w", err) } if req.Title != "" { c.Title = req.Title } if req.Message != "" { c.Message = req.Message } c.Description = req.Description if req.CampaignType != "" { c.CampaignType = campaign.CampaignType(req.CampaignType) } if req.Audience != "" { c.Audience = req.Audience } if req.TriggerRules != "" { c.TriggerRules = req.TriggerRules } if req.TemplateParams != "" { c.TemplateParams = req.TemplateParams } if req.Enabled != nil { c.Enabled = *req.Enabled } if req.TriggerOnlyDuringBusinessHours != nil { c.TriggerOnlyDuringBusinessHours = *req.TriggerOnlyDuringBusinessHours } if err := s.campaignSvc.Update(ctx, c.ID, map[string]interface{}{ "title": c.Title, "message": c.Message, "description": c.Description, "campaign_type": c.CampaignType, "audience": c.Audience, "trigger_rules": c.TriggerRules, "template_params": c.TemplateParams, "enabled": c.Enabled, "trigger_only_during_business_hours": c.TriggerOnlyDuringBusinessHours, }); err != nil { applogger.L().Errorf("failed to update campaign: %v", err) return nil, fmt.Errorf("failed to update campaign: %w", err) } return c, nil } // Delete soft-deletes a campaign scoped to an account. func (s *CampaignService) Delete(ctx context.Context, id, accountID uint) error { if err := s.campaignSvc.Delete(ctx, id); err != nil { return fmt.Errorf("failed to delete campaign: %w", err) } return nil } // Start triggers a campaign execution. func (s *CampaignService) Start(ctx context.Context, id, accountID uint) error { c, err := s.campaignRepo.FindByIDAndAccount(ctx, id, accountID) if err != nil { return fmt.Errorf("campaign not found: %w", err) } if err := s.campaignSvc.TriggerCampaign(ctx, c.ID); err != nil { return fmt.Errorf("failed to trigger campaign: %w", err) } return nil } // Stop marks a campaign as completed. func (s *CampaignService) Stop(ctx context.Context, id, accountID uint) error { c, err := s.campaignRepo.FindByIDAndAccount(ctx, id, accountID) if err != nil { return fmt.Errorf("campaign not found: %w", err) } if err := s.campaignSvc.MarkCompleted(ctx, c.ID); err != nil { return fmt.Errorf("failed to stop campaign: %w", err) } return nil }