package campaign import ( "context" "encoding/json" "fmt" "github.com/gochat/gochat/internal/model" applogger "github.com/gochat/gochat/pkg/logger" "gorm.io/gorm" ) // CampaignService provides business logic for campaign operations. // Reference: Chatwoot CampaignService — CRUD + campaign triggering. type CampaignService struct { db *gorm.DB } func NewCampaignService(db *gorm.DB) *CampaignService { return &CampaignService{db: db} } // Create creates a new campaign. func (s *CampaignService) Create(ctx context.Context, campaign *Campaign) error { return s.db.WithContext(ctx).Create(campaign).Error } // GetByID retrieves a campaign by ID. func (s *CampaignService) GetByID(ctx context.Context, id uint) (*Campaign, error) { var c Campaign if err := s.db.WithContext(ctx).First(&c, id).Error; err != nil { return nil, err } return &c, nil } // ListByAccount returns campaigns for an account with pagination. func (s *CampaignService) ListByAccount(ctx context.Context, accountID uint, offset, limit int) ([]Campaign, int64, error) { var campaigns []Campaign var count int64 db := s.db.WithContext(ctx).Model(&Campaign{}).Where("account_id = ?", accountID) db.Count(&count) if err := db.Offset(offset).Limit(limit).Order("created_at DESC").Find(&campaigns).Error; err != nil { return nil, 0, err } return campaigns, count, nil } // ListByInbox returns campaigns for a specific inbox. func (s *CampaignService) ListByInbox(ctx context.Context, accountID, inboxID uint) ([]Campaign, error) { var campaigns []Campaign err := s.db.WithContext(ctx). Where("account_id = ? AND inbox_id = ?", accountID, inboxID). Order("created_at DESC").Find(&campaigns).Error return campaigns, err } // Update updates campaign fields. func (s *CampaignService) Update(ctx context.Context, id uint, updates map[string]interface{}) error { return s.db.WithContext(ctx).Model(&Campaign{}).Where("id = ?", id).Updates(updates).Error } // Delete soft-deletes a campaign. func (s *CampaignService) Delete(ctx context.Context, id uint) error { return s.db.WithContext(ctx).Delete(&Campaign{}, id).Error } // MarkCompleted transitions a campaign to completed status. func (s *CampaignService) MarkCompleted(ctx context.Context, id uint) error { return s.Update(ctx, id, map[string]interface{}{ "campaign_status": CampaignStatusCompleted, }) } // TriggerCampaign executes a campaign by creating conversations for the target audience. // Reference: Chatwoot CampaignService.trigger — builds conversation from campaign. func (s *CampaignService) TriggerCampaign(ctx context.Context, campaignID uint) error { campaign, err := s.GetByID(ctx, campaignID) if err != nil { return fmt.Errorf("campaign: get campaign: %w", err) } if !campaign.Enabled { applogger.L().Info("campaign: campaign is disabled, skipping trigger", "campaign_id", campaignID) return nil } builder := NewCampaignConversationBuilder(s.db) return builder.Build(ctx, campaign) } // CampaignConversationBuilder builds a conversation from a campaign trigger. // Reference: Chatwoot CampaignListener — on campaign_triggered event -> builds conversation. type CampaignConversationBuilder struct { db *gorm.DB } func NewCampaignConversationBuilder(db *gorm.DB) *CampaignConversationBuilder { return &CampaignConversationBuilder{db: db} } // Build creates a new conversation from the campaign for each contact in the audience. // The audience field (JSONB) contains contact IDs or filter criteria. func (b *CampaignConversationBuilder) Build(ctx context.Context, campaign *Campaign) error { // Parse audience to extract contact IDs var audienceData struct { ContactIDs []uint `json:"contact_ids"` } if err := json.Unmarshal([]byte(campaign.Audience), &audienceData); err != nil { return fmt.Errorf("campaign: parse audience: %w", err) } for _, contactID := range audienceData.ContactIDs { conv := &model.Conversation{ AccountID: campaign.AccountID, InboxID: campaign.InboxID, ContactID: contactID, Status: "open", ChannelType: "campaign", } if campaign.SenderID != nil { conv.AssigneeID = campaign.SenderID } if err := b.db.WithContext(ctx).Create(conv).Error; err != nil { applogger.L().Error("campaign: failed to create conversation for contact", "campaign_id", campaign.ID, "contact_id", contactID, "error", err) continue } // Create the initial campaign message in the conversation msg := &model.Message{ ConversationID: conv.ID, AccountID: campaign.AccountID, InboxID: campaign.InboxID, Content: campaign.Message, ContentType: "template", MessageType: "outgoing", SenderType: "agent", } if campaign.SenderID != nil { msg.SenderID = campaign.SenderID } if err := b.db.WithContext(ctx).Create(msg).Error; err != nil { applogger.L().Error("campaign: failed to create campaign message", "campaign_id", campaign.ID, "conversation_id", conv.ID, "error", err) continue } applogger.L().Info("campaign: created conversation from campaign", "campaign_id", campaign.ID, "conversation_id", conv.ID, "contact_id", contactID) } return nil }