Files
gochat/internal/service/campaign_service.go
T
2026-06-04 15:44:48 +08:00

199 lines
7.5 KiB
Go

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
}