diff --git a/backend/internal/app/bootstrap.go b/backend/internal/app/bootstrap.go index fb54e6b7..0b5e59cd 100644 --- a/backend/internal/app/bootstrap.go +++ b/backend/internal/app/bootstrap.go @@ -581,7 +581,13 @@ func Bootstrap(env string) (*App, error) { if err := copilotConfigService.Initialize(context.Background()); err != nil { return nil, fmt.Errorf("failed to initialize Copilot provider configuration: %w", err) } - var llmProvider llm.Provider = copilotProviderManager + var llmProvider llm.Provider + if llm.IsFakeAIEnabled() { + llmProvider = llm.NewFakeLLMProvider() + applogger.L().Info("FakeAI provider enabled — using canned LLM responses") + } else { + llmProvider = copilotProviderManager + } messageService := service.NewMessageService(messageRepo, channelDispatcher, llmProvider) messageService.SetWorkerPool(workerPool) diff --git a/backend/internal/handler/api/v1/campaign_handler.go b/backend/internal/handler/api/v1/campaign_handler.go index e0559a45..4a63a505 100644 --- a/backend/internal/handler/api/v1/campaign_handler.go +++ b/backend/internal/handler/api/v1/campaign_handler.go @@ -78,15 +78,27 @@ func (h *CampaignHandler) Create(c *gin.Context) { return } - // Chatwoot: params.require(:campaign) → request body must be {"campaign": {...}} + // Accept both wrapped {"campaign": {...}} (Chatwoot style) and flat {...} payloads. + // The frontend sends flat payload, so we prefer that format. + var req service.CreateCampaignRequest var wrapper struct { Campaign service.CreateCampaignRequest `json:"campaign"` } - if err := c.ShouldBindJSON(&wrapper); err != nil { - response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) + rawBody, err := c.GetRawData() + if err != nil { + response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "cannot read request body") return } - req := wrapper.Campaign + // Try wrapped format first (Chatwoot style) + if err := json.Unmarshal(rawBody, &wrapper); err == nil && wrapper.Campaign.Title != "" { + req = wrapper.Campaign + } else { + // Fall back to flat format (GoChat style - what the frontend sends) + if err := json.Unmarshal(rawBody, &req); err != nil { + response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) + return + } + } campaign, svcErr := h.svc.Create(c.Request.Context(), accountID, req) if svcErr != nil { diff --git a/backend/internal/service/analytics_query_helpers.go b/backend/internal/service/analytics_query_helpers.go index d7ac78d4..bf285bdb 100644 --- a/backend/internal/service/analytics_query_helpers.go +++ b/backend/internal/service/analytics_query_helpers.go @@ -119,11 +119,11 @@ func (s *AnalyticsService) conversationCountTimeseries(ctx context.Context, acco return nil, err } var conversations []model.Conversation - q := db.WithContext(ctx).Model(&model.Conversation{}).Where("account_id = ?", accountID) + q := db.WithContext(ctx).Model(&model.Conversation{}).Where("conversations.account_id = ?", accountID) if resolved { - q = q.Where("resolved_at IS NOT NULL AND resolved_at >= ? AND resolved_at < ?", since, until) + q = q.Where("conversations.resolved_at IS NOT NULL AND conversations.resolved_at >= ? AND conversations.resolved_at < ?", since, until) } else { - q = q.Where("created_at >= ? AND created_at < ?", since, until) + q = q.Where("conversations.created_at >= ? AND conversations.created_at < ?", since, until) } q = applyConversationDimension(q, reportType, id) if err := q.Find(&conversations).Error; err != nil {