package v1 import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/automation" "github.com/gochat/gochat/pkg/response" ) // MacroHandler handles macro API endpoints. // Reference: Chatwoot MacrosController — CRUD + execute type MacroHandler struct { svc *automation.MacroService } // NewMacroHandler creates a new MacroHandler. func NewMacroHandler(svc *automation.MacroService) *MacroHandler { return &MacroHandler{svc: svc} } // List retrieves all macros for an account, respecting visibility. // GET /api/v1/accounts/:account_id/macros func (h *MacroHandler) List(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } userID := getUserID(c) macros, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, userID) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, gin.H{ "payload": macros, }) } // Get retrieves a single macro by ID. // GET /api/v1/accounts/:account_id/macros/:id func (h *MacroHandler) Get(c *gin.Context) { macroID, err := parseUintParam(c, "macro_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } macro, svcErr := h.svc.GetByID(c.Request.Context(), macroID) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, macro) } // Create creates a new macro. // POST /api/v1/accounts/:account_id/macros func (h *MacroHandler) Create(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } var macro automation.Macro if err := c.ShouldBindJSON(¯o); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } macro.AccountID = accountID macro.CreatedByID = getUserID(c) macro.UpdatedByID = getUserID(c) // set_visibility: Chatwoot forces visibility=personal if user role is agent // Reference: Chatwoot macro.rb set_visibility — self.visibility = :personal if user.agent? role := getRole(c) if role == "agent" { macro.Visibility = automation.MacroVisibilityPersonal } if svcErr := h.svc.Create(c.Request.Context(), ¯o); svcErr != nil { handleServiceError(c, svcErr) return } response.Created(c, macro) } // Update updates an existing macro. // PUT /api/v1/accounts/:account_id/macros/:id func (h *MacroHandler) Update(c *gin.Context) { macroID, err := parseUintParam(c, "macro_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } var macro automation.Macro if err := c.ShouldBindJSON(¯o); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } macro.ID = macroID macro.UpdatedByID = getUserID(c) if svcErr := h.svc.Update(c.Request.Context(), ¯o); svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, macro) } // Delete soft-deletes a macro. // DELETE /api/v1/accounts/:account_id/macros/:id func (h *MacroHandler) Delete(c *gin.Context) { macroID, err := parseUintParam(c, "macro_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } if svcErr := h.svc.Delete(c.Request.Context(), macroID); svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, gin.H{ "id": strconv.FormatUint(uint64(macroID), 10), "deleted": true, }) } // Execute runs a macro on a conversation. // POST /api/v1/accounts/:account_id/macros/:id/execute // Reference: Chatwoot MacrosController#execute — MacroService.Execute returns error only func (h *MacroHandler) Execute(c *gin.Context) { macroID, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid macro id") return } accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } var body struct { ConversationID uint `json:"conversation_id"` } if err := c.ShouldBindJSON(&body); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "conversation_id is required") return } userID := getUserID(c) if svcErr := h.svc.Execute(c.Request.Context(), accountID, body.ConversationID, macroID, userID); svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, gin.H{ "success": true, "macro_id": strconv.FormatUint(uint64(macroID), 10), "conversation_id": strconv.FormatUint(uint64(body.ConversationID), 10), }) } // Clone duplicates a macro. // POST /api/v1/accounts/:account_id/macros/:id/clone // Reference: Chatwoot MacrosController does not have clone, but AutomationRulesController does. // gochat adds clone for macros per M6 requirements. func (h *MacroHandler) Clone(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } macroID, err := parseUintParam(c, "macro_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } _ = accountID // validated but not used — authorization enforced by middleware cloned, svcErr := h.svc.Clone(c.Request.Context(), macroID) if svcErr != nil { handleServiceError(c, svcErr) return } response.Created(c, cloned) } // ToggleActive toggles the active state of a macro. // POST /api/v1/accounts/:account_id/macros/:id/toggle_active // Reference: Chatwoot does not have toggle_active for macros; gochat adds this per M6 requirements. func (h *MacroHandler) ToggleActive(c *gin.Context) { macroID, err := parseUintParam(c, "macro_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } var req struct { Active bool `json:"active"` } if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "active field is required") return } if svcErr := h.svc.ToggleActive(c.Request.Context(), macroID, req.Active); svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, gin.H{ "id": strconv.FormatUint(uint64(macroID), 10), "active": req.Active, }) }