chore: stabilize chatwoot parity baseline

This commit is contained in:
2026-06-04 18:12:50 +08:00
parent 8ac150bc7b
commit 42cdab880c
39 changed files with 1028 additions and 696 deletions
+32 -33
View File
@@ -2,7 +2,6 @@ package v1
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/service"
@@ -66,13 +65,13 @@ func (h *AccountHandler) List(c *gin.Context) {
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id} [get]
func (h *AccountHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
id := parseAccountIDParam(c)
if id == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
account, err := h.svc.GetByID(c.Request.Context(), uint(id))
account, err := h.svc.GetByID(c.Request.Context(), id)
if err != nil {
applogger.L().Errorf("Get account: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrAccountNotFound, "account not found")
@@ -131,8 +130,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id} [put]
func (h *AccountHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
id := parseAccountIDParam(c)
if id == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
@@ -143,7 +142,7 @@ func (h *AccountHandler) Update(c *gin.Context) {
return
}
account, err := h.svc.Update(c.Request.Context(), uint(id), req)
account, err := h.svc.Update(c.Request.Context(), id, req)
if err != nil {
applogger.L().Errorf("Update account: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update account")
@@ -166,13 +165,13 @@ func (h *AccountHandler) Update(c *gin.Context) {
// @Security ApiKeyAuth
// @Router /api/v1/accounts/{id} [delete]
func (h *AccountHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
id := parseAccountIDParam(c)
if id == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
if err := h.svc.Delete(c.Request.Context(), uint(id)); err != nil {
if err := h.svc.Delete(c.Request.Context(), id); err != nil {
applogger.L().Errorf("Delete account: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete account")
return
@@ -185,14 +184,14 @@ func (h *AccountHandler) Delete(c *gin.Context) {
// GET /api/v1/accounts/:id/users
// Reference: Chatwoot Accounts::AccountUsersController#index
func (h *AccountHandler) ListUsers(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
pg := pagination.Parse(c)
users, total, err := h.svc.ListUsers(c.Request.Context(), uint(accountID), pg.Offset, pg.PerPage)
users, total, err := h.svc.ListUsers(c.Request.Context(), accountID, pg.Offset, pg.PerPage)
if err != nil {
applogger.L().Errorf("List account users: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list account users")
@@ -206,8 +205,8 @@ func (h *AccountHandler) ListUsers(c *gin.Context) {
// POST /api/v1/accounts/:id/users
// Reference: Chatwoot Accounts::AccountUsersController#create
func (h *AccountHandler) AddUser(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
@@ -218,7 +217,7 @@ func (h *AccountHandler) AddUser(c *gin.Context) {
return
}
if err := h.svc.AddUser(c.Request.Context(), uint(accountID), req); err != nil {
if err := h.svc.AddUser(c.Request.Context(), accountID, req); err != nil {
applogger.L().Errorf("Add user to account: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to add user to account")
return
@@ -231,19 +230,19 @@ func (h *AccountHandler) AddUser(c *gin.Context) {
// DELETE /api/v1/accounts/:id/users/:user_id
// Reference: Chatwoot Accounts::AccountUsersController#destroy
func (h *AccountHandler) RemoveUser(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
userID, err := strconv.ParseUint(c.Param("user_id"), 10, 64)
userID, err := parseUintParam(c, "user_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user id")
return
}
if err := h.svc.RemoveUser(c.Request.Context(), uint(accountID), uint(userID)); err != nil {
if err := h.svc.RemoveUser(c.Request.Context(), accountID, userID); err != nil {
applogger.L().Errorf("Remove user from account: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to remove user from account")
return
@@ -271,14 +270,14 @@ func (h *AccountHandler) GetAll(c *gin.Context) {
// GET /api/v1/accounts/:id/agents
// Reference: Chatwoot app/controllers/api/v1/accounts/agents_controller.rb#index
func (h *AccountHandler) GetAgents(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
pg := pagination.Parse(c)
agents, total, err := h.svc.GetAgents(c.Request.Context(), uint(accountID), pg.Offset, pg.PerPage)
agents, total, err := h.svc.GetAgents(c.Request.Context(), accountID, pg.Offset, pg.PerPage)
if err != nil {
applogger.L().Errorf("GetAgents for account %d: %v", accountID, err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list agents")
@@ -292,8 +291,8 @@ func (h *AccountHandler) GetAgents(c *gin.Context) {
// PUT /api/v1/accounts/:id/settings
// Reference: Chatwoot AccountsController#update (settings subset)
func (h *AccountHandler) UpdateSettings(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
id := parseAccountIDParam(c)
if id == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
@@ -304,7 +303,7 @@ func (h *AccountHandler) UpdateSettings(c *gin.Context) {
return
}
account, err := h.svc.UpdateSettings(c.Request.Context(), uint(id), req)
account, err := h.svc.UpdateSettings(c.Request.Context(), id, req)
if err != nil {
applogger.L().Errorf("Update account settings: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update account settings")
@@ -320,14 +319,14 @@ func (h *AccountHandler) UpdateSettings(c *gin.Context) {
// UpdateActiveAt updates the active_at timestamp for the current user in an account.
// POST /api/v1/accounts/:id/update_active_at
func (h *AccountHandler) UpdateActiveAt(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
id := parseAccountIDParam(c)
if id == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
userID := getUserID(c)
if err := h.svc.UpdateActiveAt(c.Request.Context(), uint(id), userID); err != nil {
if err := h.svc.UpdateActiveAt(c.Request.Context(), id, userID); err != nil {
applogger.L().Errorf("UpdateActiveAt for account %d, user %d: %v", id, userID, err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update active_at")
return
@@ -339,14 +338,14 @@ func (h *AccountHandler) UpdateActiveAt(c *gin.Context) {
// CacheKeys returns cache key identifiers for frontend cache invalidation.
// GET /api/v1/accounts/:id/cache_keys
func (h *AccountHandler) CacheKeys(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
id := parseAccountIDParam(c)
if id == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account id")
return
}
userID := getUserID(c)
keys, err := h.svc.CacheKeys(c.Request.Context(), uint(id), userID)
keys, err := h.svc.CacheKeys(c.Request.Context(), id, userID)
if err != nil {
applogger.L().Errorf("CacheKeys for account %d, user %d: %v", id, userID, err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get cache keys")
@@ -354,4 +353,4 @@ func (h *AccountHandler) CacheKeys(c *gin.Context) {
}
response.OK(c, keys)
}
}
@@ -13,8 +13,8 @@ import (
// AssignableAgentHandler handles endpoints for finding agents assignable to conversations.
// Reference: Chatwoot app/controllers/api/v1/accounts/assignable_agents_controller.rb
// Two routes:
// 1. GET /api/v1/accounts/:account_id/assignable_agents (with inbox_ids[] query param)
// 2. GET /api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents (single inbox)
// 1. GET /api/v1/accounts/:account_id/assignable_agents (with inbox_ids[] query param)
// 2. GET /api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents (single inbox)
type AssignableAgentHandler struct {
svc *service.AssignableAgentService
}
@@ -26,12 +26,18 @@ func NewAssignableAgentHandler(svc *service.AssignableAgentService) *AssignableA
// List returns agents that can be assigned to conversations in the specified inbox(es).
// Supports two modes:
// 1. Standalone resource: GET /api/v1/accounts/:account_id/assignable_agents?inbox_ids[]=1&inbox_ids[]=2
// (Ref: Chatwoot resources :assignable_agents, only: [:index] — takes inbox_ids[] query param)
// 2. Inbox member action: GET /api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents
// (Ref: Chatwoot get :assignable_agents, on: :member on inboxes)
// 1. Standalone resource: GET /api/v1/accounts/:account_id/assignable_agents?inbox_ids[]=1&inbox_ids[]=2
// (Ref: Chatwoot resources :assignable_agents, only: [:index] — takes inbox_ids[] query param)
// 2. Inbox member action: GET /api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents
// (Ref: Chatwoot get :assignable_agents, on: :member on inboxes)
//
// 返回带workload信息的agent列表,按workload升序排序(最少conversations优先)。
func (h *AssignableAgentHandler) List(c *gin.Context) {
if h.svc == nil {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "assignable agent service unavailable")
return
}
accountID := getAccountID(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
@@ -70,9 +76,9 @@ func (h *AssignableAgentHandler) List(c *gin.Context) {
agents, svcErr := h.svc.GetAssignableAgents(c.Request.Context(), accountID, inboxIDs)
if svcErr != nil {
handleServiceError(c, svcErr)
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to list assignable agents")
return
}
response.OK(c, agents)
}
}
@@ -40,7 +40,7 @@ func (h *AutoReplyRuleHandler) Create(c *gin.Context) {
result, err := h.svc.CreateRule(c.Request.Context(), uint(accountID), &req)
if err != nil {
applogger.L().Errorf("AutoReplyRuleHandler.Create failed: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, "create_failed", "Failed to create auto-reply rule")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, "create_failed", "Failed to create auto-reply rule")
return
}
@@ -96,7 +96,7 @@ func (h *AutoReplyRuleHandler) Update(c *gin.Context) {
result, err := h.svc.UpdateRule(c.Request.Context(), uint(accountID), uint(ruleID), &req)
if err != nil {
applogger.L().Errorf("AutoReplyRuleHandler.Update failed: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, "update_failed", "Failed to update auto-reply rule")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, "update_failed", "Failed to update auto-reply rule")
return
}
@@ -120,7 +120,7 @@ func (h *AutoReplyRuleHandler) Delete(c *gin.Context) {
if err := h.svc.DeleteRule(c.Request.Context(), uint(accountID), uint(ruleID)); err != nil {
applogger.L().Errorf("AutoReplyRuleHandler.Delete failed: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, "delete_failed", "Failed to delete auto-reply rule")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, "delete_failed", "Failed to delete auto-reply rule")
return
}
@@ -44,6 +44,11 @@ func (h *AutomationRuleHandler) List(c *gin.Context) {
// Get retrieves a single automation rule by ID.
// GET /api/v1/accounts/:account_id/automation_rules/:id
func (h *AutomationRuleHandler) Get(c *gin.Context) {
if _, err := parseUintParam(c, "account_id"); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
automationID, err := parseUintParam(c, "automation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
@@ -73,6 +78,10 @@ func (h *AutomationRuleHandler) Create(c *gin.Context) {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
if rule.Name == "" || rule.EventName == "" {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "name and event_name are required")
return
}
rule.AccountID = accountID
@@ -87,6 +96,11 @@ func (h *AutomationRuleHandler) Create(c *gin.Context) {
// Update updates an existing automation rule.
// PUT /api/v1/accounts/:account_id/automation_rules/:id
func (h *AutomationRuleHandler) Update(c *gin.Context) {
if _, err := parseUintParam(c, "account_id"); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
automationID, err := parseUintParam(c, "automation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
@@ -98,6 +112,10 @@ func (h *AutomationRuleHandler) Update(c *gin.Context) {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
if rule.Name == "" || rule.EventName == "" {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "name and event_name are required")
return
}
rule.ID = automationID
@@ -112,6 +130,11 @@ func (h *AutomationRuleHandler) Update(c *gin.Context) {
// Delete soft-deletes an automation rule.
// DELETE /api/v1/accounts/:account_id/automation_rules/:id
func (h *AutomationRuleHandler) Delete(c *gin.Context) {
if _, err := parseUintParam(c, "account_id"); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
automationID, err := parseUintParam(c, "automation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
@@ -132,6 +155,11 @@ func (h *AutomationRuleHandler) Delete(c *gin.Context) {
// Clone duplicates an automation rule.
// POST /api/v1/accounts/:account_id/automation_rules/:id/clone
func (h *AutomationRuleHandler) Clone(c *gin.Context) {
if _, err := parseUintParam(c, "account_id"); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
automationID, err := parseUintParam(c, "automation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
@@ -151,6 +179,11 @@ func (h *AutomationRuleHandler) Clone(c *gin.Context) {
// POST /api/v1/accounts/:account_id/automation_rules/:id/toggle_active
// Reference: Chatwoot does not have a toggle_active endpoint; gochat adds this per M6 requirements.
func (h *AutomationRuleHandler) ToggleActive(c *gin.Context) {
if _, err := parseUintParam(c, "account_id"); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
automationID, err := parseUintParam(c, "automation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
@@ -158,21 +191,24 @@ func (h *AutomationRuleHandler) ToggleActive(c *gin.Context) {
}
var req struct {
Active bool `json:"active"`
Active *bool `json:"active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "active field is required")
return
}
if req.Active == nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, "active field is required")
return
}
if svcErr := h.svc.ToggleActive(c.Request.Context(), automationID, req.Active); svcErr != nil {
if svcErr := h.svc.ToggleActive(c.Request.Context(), automationID, *req.Active); svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, gin.H{
"id": strconv.FormatUint(uint64(automationID), 10),
"active": req.Active,
"active": *req.Active,
})
}
+15 -15
View File
@@ -69,7 +69,7 @@ func (h *BotRuleHandler) ListByBot(c *gin.Context) {
// Get retrieves a single bot rule by ID.
// GET /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/bot_rules/:id
func (h *BotRuleHandler) Get(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "rule_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -99,12 +99,12 @@ func (h *BotRuleHandler) Create(c *gin.Context) {
}
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
EventName automation.BotRuleEventType `json:"event_name" binding:"required"`
Conditions automation.Conditions `json:"conditions"`
Actions automation.Actions `json:"actions"`
Status automation.BotRuleStatus `json:"status"`
Conditions automation.Conditions `json:"conditions"`
Actions automation.Actions `json:"actions"`
Status automation.BotRuleStatus `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
@@ -137,7 +137,7 @@ func (h *BotRuleHandler) Create(c *gin.Context) {
// Update updates an existing bot rule.
// PUT /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/bot_rules/:id
func (h *BotRuleHandler) Update(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "rule_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -150,12 +150,12 @@ func (h *BotRuleHandler) Update(c *gin.Context) {
}
var req struct {
Name string `json:"name"`
Description string `json:"description"`
Name string `json:"name"`
Description string `json:"description"`
EventName automation.BotRuleEventType `json:"event_name"`
Conditions automation.Conditions `json:"conditions"`
Actions automation.Actions `json:"actions"`
Status automation.BotRuleStatus `json:"status"`
Conditions automation.Conditions `json:"conditions"`
Actions automation.Actions `json:"actions"`
Status automation.BotRuleStatus `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
@@ -193,7 +193,7 @@ func (h *BotRuleHandler) Update(c *gin.Context) {
// Delete soft-deletes a bot rule.
// DELETE /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/bot_rules/:id
func (h *BotRuleHandler) Delete(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "rule_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -210,7 +210,7 @@ func (h *BotRuleHandler) Delete(c *gin.Context) {
// ToggleStatus toggles the active/inactive status of a bot rule.
// PATCH /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/bot_rules/:id/status
func (h *BotRuleHandler) ToggleStatus(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "rule_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -235,7 +235,7 @@ func (h *BotRuleHandler) ToggleStatus(c *gin.Context) {
// Clone duplicates a bot rule.
// POST /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/bot_rules/:id/clone
func (h *BotRuleHandler) Clone(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "rule_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -67,14 +67,14 @@ func (s *BotRuleHandlerTestSuite) SetupSuite() {
// Register routes matching the handler's expected URL patterns
accountGroup := r.Group("/api/v1/accounts/:account_id/agent_bots/:agent_bot_id")
{
accountGroup.GET("/bot_rules_configs", s.handler.List) // List by account
accountGroup.GET("/bot_rules", s.handler.ListByBot) // List by bot
accountGroup.GET("/bot_rules/:agent_bot_id", s.handler.Get) // Get single
accountGroup.POST("/bot_rules", s.handler.Create) // Create
accountGroup.PUT("/bot_rules/:agent_bot_id", s.handler.Update) // Update
accountGroup.DELETE("/bot_rules/:agent_bot_id", s.handler.Delete) // Delete
accountGroup.PATCH("/bot_rules/:agent_bot_id/status", s.handler.ToggleStatus) // Toggle status
accountGroup.POST("/bot_rules/:agent_bot_id/clone", s.handler.Clone) // Clone
accountGroup.GET("/bot_rules_configs", s.handler.List) // List by account
accountGroup.GET("/bot_rules", s.handler.ListByBot) // List by bot
accountGroup.GET("/bot_rules/:rule_id", s.handler.Get) // Get single
accountGroup.POST("/bot_rules", s.handler.Create) // Create
accountGroup.PUT("/bot_rules/:rule_id", s.handler.Update) // Update
accountGroup.DELETE("/bot_rules/:rule_id", s.handler.Delete) // Delete
accountGroup.PATCH("/bot_rules/:rule_id/status", s.handler.ToggleStatus) // Toggle status
accountGroup.POST("/bot_rules/:rule_id/clone", s.handler.Clone) // Clone
}
}
@@ -563,4 +563,4 @@ func TestBotRuleHandler_NilServicePanics(t *testing.T) {
func TestBotRuleHandlerTestSuite(t *testing.T) {
suite.Run(t, new(BotRuleHandlerTestSuite))
}
}
@@ -69,7 +69,7 @@ func (h *BotTriggerConfigHandler) ListByBot(c *gin.Context) {
// Get retrieves a single trigger config by ID.
// GET /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:id
func (h *BotTriggerConfigHandler) Get(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "trigger_config_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -99,12 +99,12 @@ func (h *BotTriggerConfigHandler) Create(c *gin.Context) {
}
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
EventName automation.BotRuleEventType `json:"event_name" binding:"required"`
Conditions automation.TriggerConditions `json:"conditions"`
QueryOperator string `json:"query_operator"`
Active bool `json:"active"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
EventName automation.BotRuleEventType `json:"event_name" binding:"required"`
Conditions automation.TriggerConditions `json:"conditions"`
QueryOperator string `json:"query_operator"`
Active bool `json:"active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
@@ -143,7 +143,7 @@ func (h *BotTriggerConfigHandler) Create(c *gin.Context) {
// Update updates an existing trigger config.
// PUT /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:id
func (h *BotTriggerConfigHandler) Update(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "trigger_config_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -156,12 +156,12 @@ func (h *BotTriggerConfigHandler) Update(c *gin.Context) {
}
var req struct {
Name string `json:"name"`
Description string `json:"description"`
EventName automation.BotRuleEventType `json:"event_name"`
Conditions automation.TriggerConditions `json:"conditions"`
QueryOperator string `json:"query_operator"`
Active bool `json:"active"`
Name string `json:"name"`
Description string `json:"description"`
EventName automation.BotRuleEventType `json:"event_name"`
Conditions automation.TriggerConditions `json:"conditions"`
QueryOperator string `json:"query_operator"`
Active bool `json:"active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
@@ -197,7 +197,7 @@ func (h *BotTriggerConfigHandler) Update(c *gin.Context) {
// Delete soft-deletes a trigger config.
// DELETE /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:id
func (h *BotTriggerConfigHandler) Delete(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "trigger_config_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -214,7 +214,7 @@ func (h *BotTriggerConfigHandler) Delete(c *gin.Context) {
// ToggleActive toggles the active state of a trigger config.
// PATCH /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:id/active
func (h *BotTriggerConfigHandler) ToggleActive(c *gin.Context) {
id, err := parseUintParam(c, "id")
id, err := parseUintAnyParam(c, "trigger_config_id", "id", "agent_bot_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -123,13 +123,13 @@ func (s *BotTriggerConfigHandlerTestSuite) setupRouter() *gin.Engine {
{
botGroup.GET("", s.handler.ListByBot)
botGroup.POST("", s.handler.Create)
botGroup.GET("/:agent_bot_id", s.handler.Get)
botGroup.PUT("/:agent_bot_id", s.handler.Update)
botGroup.DELETE("/:agent_bot_id", s.handler.Delete)
botGroup.GET("/:trigger_config_id", s.handler.Get)
botGroup.PUT("/:trigger_config_id", s.handler.Update)
botGroup.DELETE("/:trigger_config_id", s.handler.Delete)
}
// ToggleActive route (handler method exists but not in production router yet)
r.PATCH("/api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:agent_bot_id/active", s.handler.ToggleActive)
r.PATCH("/api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:trigger_config_id/active", s.handler.ToggleActive)
return r
}
@@ -714,10 +714,10 @@ func (s *BotTriggerConfigHandlerTestSuite) TestToggleActive_MalformedJSON() {
// createTestConfig creates a BotTriggerConfig directly via the service for test seeding.
func (s *BotTriggerConfigHandlerTestSuite) createTestConfig(name string, eventName automation.BotRuleEventType, active bool) *automation.BotTriggerConfig {
config := &automation.BotTriggerConfig{
AccountID: s.accountID,
AgentBotID: s.agentBotID,
Name: name,
EventName: eventName,
AccountID: s.accountID,
AgentBotID: s.agentBotID,
Name: name,
EventName: eventName,
Conditions: automation.TriggerConditions{
{Attribute: "status", FilterOperator: "equal", Values: []string{"open"}, QueryOperator: "and"},
},
@@ -727,4 +727,4 @@ func (s *BotTriggerConfigHandlerTestSuite) createTestConfig(name string, eventNa
err := s.svc.Create(context.Background(), config)
s.Require().NoError(err)
return config
}
}
@@ -38,7 +38,7 @@ func (h *CaptainAssistantResponseHandler) ProcessResponse(c *gin.Context) {
result, err := h.svc.ProcessResponse(c.Request.Context(), uint(accountID), &req)
if err != nil {
applogger.L().Errorf("Process assistant response: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to process assistant response")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to process assistant response")
return
}
@@ -164,10 +164,10 @@ func (h *CaptainAssistantResponseHandler) Create(c *gin.Context) {
}
var req struct {
Question string `json:"question" validate:"required"`
Answer string `json:"answer" validate:"required"`
AssistantID uint `json:"assistant_id" validate:"required"`
Status string `json:"status"`
Question string `json:"question" validate:"required"`
Answer string `json:"answer" validate:"required"`
AssistantID uint `json:"assistant_id" validate:"required"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
@@ -30,10 +30,10 @@ import (
type CaptainCustomToolCRUDTestSuite struct {
suite.Suite
router *gin.Engine // unified router with :id=account_id, :tool_id=tool_id
handler *CaptainCustomToolHandler
db *gorm.DB
account *model.Account
router *gin.Engine // unified router with :id=account_id, :tool_id=tool_id
handler *CaptainCustomToolHandler
db *gorm.DB
account *model.Account
}
func (s *CaptainCustomToolCRUDTestSuite) SetupSuite() {
@@ -62,7 +62,7 @@ func (s *CaptainCustomToolCRUDTestSuite) SetupSuite() {
// Unified router: :id = account_id, :tool_id = tool_id
s.router = gin.New()
s.router.RedirectTrailingSlash = false
accGroup := s.router.Group("/api/v1/accounts/:custom_tool_id")
accGroup := s.router.Group("/api/v1/accounts/:account_id")
{
ctGroup := accGroup.Group("/captain/custom_tools")
{
@@ -103,7 +103,7 @@ func (s *CaptainCustomToolCRUDTestSuite) makeRequest(method, path string, body i
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
s.router.ServeHTTP(w, req)
s.router.ServeHTTP(w, req)
return w
}
@@ -171,7 +171,7 @@ func (s *CaptainCustomToolCRUDTestSuite) TestCreate_无效JSON返回400() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", s.accountPath()+"/captain/custom_tools/", bytes.NewReader([]byte("{invalid}")))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
@@ -313,4 +313,4 @@ func (s *CaptainCustomToolCRUDTestSuite) TestDelete_不存在的ID() {
func TestCaptainCustomToolCRUDSuite(t *testing.T) {
suite.Run(t, new(CaptainCustomToolCRUDTestSuite))
}
}
@@ -2,7 +2,6 @@ package v1
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/service"
@@ -25,8 +24,8 @@ func NewCaptainCustomToolHandler(svc *service.CaptainCustomToolService) *Captain
// Create creates a new custom tool.
// POST /api/v1/accounts/:account_id/captain_custom_tools
func (h *CaptainCustomToolHandler) Create(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
@@ -37,7 +36,7 @@ func (h *CaptainCustomToolHandler) Create(c *gin.Context) {
return
}
tool, err := h.svc.Create(c.Request.Context(), uint(accountID), &req)
tool, err := h.svc.Create(c.Request.Context(), accountID, &req)
if err != nil {
applogger.L().Errorf("Create captain custom tool: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create custom tool")
@@ -50,13 +49,13 @@ func (h *CaptainCustomToolHandler) Create(c *gin.Context) {
// Get retrieves a custom tool by ID.
// GET /api/v1/accounts/:account_id/captain_custom_tools/:id
func (h *CaptainCustomToolHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("tool_id"), 10, 64)
id, err := parseUintAnyParam(c, "tool_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
tool, err := h.svc.Get(c.Request.Context(), uint(id))
tool, err := h.svc.Get(c.Request.Context(), id)
if err != nil {
applogger.L().Errorf("Get captain custom tool: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "custom tool not found")
@@ -69,7 +68,7 @@ func (h *CaptainCustomToolHandler) Get(c *gin.Context) {
// Update updates an existing custom tool.
// PUT /api/v1/accounts/:account_id/captain_custom_tools/:id
func (h *CaptainCustomToolHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("tool_id"), 10, 64)
id, err := parseUintAnyParam(c, "tool_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -81,10 +80,10 @@ func (h *CaptainCustomToolHandler) Update(c *gin.Context) {
return
}
tool, err := h.svc.Update(c.Request.Context(), uint(id), &req)
tool, err := h.svc.Update(c.Request.Context(), id, &req)
if err != nil {
applogger.L().Errorf("Update captain custom tool: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update custom tool")
handleServiceError(c, err)
return
}
@@ -94,13 +93,13 @@ func (h *CaptainCustomToolHandler) Update(c *gin.Context) {
// Delete deletes a custom tool.
// DELETE /api/v1/accounts/:account_id/captain_custom_tools/:id
func (h *CaptainCustomToolHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("tool_id"), 10, 64)
id, err := parseUintAnyParam(c, "tool_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
if err := h.svc.Delete(c.Request.Context(), uint(id)); err != nil {
if err := h.svc.Delete(c.Request.Context(), id); err != nil {
applogger.L().Errorf("Delete captain custom tool: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete custom tool")
return
@@ -112,14 +111,14 @@ func (h *CaptainCustomToolHandler) Delete(c *gin.Context) {
// List retrieves custom tools for an account.
// GET /api/v1/accounts/:account_id/captain_custom_tools
func (h *CaptainCustomToolHandler) List(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
p := pagination.Parse(c)
tools, count, err := h.svc.List(c.Request.Context(), uint(accountID), p.Offset, p.PerPage)
tools, count, err := h.svc.List(c.Request.Context(), accountID, p.Offset, p.PerPage)
if err != nil {
applogger.L().Errorf("List captain custom tools: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list custom tools")
@@ -132,7 +131,7 @@ func (h *CaptainCustomToolHandler) List(c *gin.Context) {
// ExecuteTool calls the external HTTP endpoint of a custom tool.
// POST /api/v1/accounts/:account_id/captain_custom_tools/:id/execute
func (h *CaptainCustomToolHandler) ExecuteTool(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("tool_id"), 10, 64)
id, err := parseUintAnyParam(c, "tool_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
@@ -144,7 +143,7 @@ func (h *CaptainCustomToolHandler) ExecuteTool(c *gin.Context) {
params = nil
}
result, err := h.svc.ExecuteTool(c.Request.Context(), uint(id), params)
result, err := h.svc.ExecuteTool(c.Request.Context(), id, params)
if err != nil {
applogger.L().Errorf("ExecuteTool: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to execute tool")
@@ -157,8 +156,8 @@ func (h *CaptainCustomToolHandler) ExecuteTool(c *gin.Context) {
// TestTool tests a custom tool with given parameters.
// POST /api/v1/accounts/:account_id/captain/custom_tools/test
func (h *CaptainCustomToolHandler) TestTool(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
@@ -169,12 +168,12 @@ func (h *CaptainCustomToolHandler) TestTool(c *gin.Context) {
return
}
result, err := h.svc.TestTool(c.Request.Context(), uint(accountID), &req)
result, err := h.svc.TestTool(c.Request.Context(), accountID, &req)
if err != nil {
applogger.L().Errorf("TestTool: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to test tool")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to test tool")
return
}
response.OK(c, result)
}
}
@@ -75,7 +75,7 @@ func (s *CaptainCustomToolTestHandlerTestSuite) SetupSuite() {
// 设置路由
s.router = gin.New()
accountsGroup := s.router.Group("/api/v1/accounts/:custom_tool_id")
accountsGroup := s.router.Group("/api/v1/accounts/:account_id")
{
captainGroup := accountsGroup.Group("/captain")
{
@@ -171,14 +171,14 @@ func (s *CaptainCustomToolTestHandlerTestSuite) TestTestTool_不存在的工具I
func (s *CaptainCustomToolTestHandlerTestSuite) TestTestTool_带POST方法和参数() {
// 创建一个POST方法的工具
postTool := &model.CaptainCustomTool{
AccountID: s.account.ID,
Title: "POST测试工具",
Slug: "post-test-tool",
EndpointURL: "https://httpbin.org/post",
HTTPMethod: "POST",
AuthType: model.ToolAuthTypeNone,
RequestTemplate: "{\"message\": \"{{.message}}\"}",
Enabled: true,
AccountID: s.account.ID,
Title: "POST测试工具",
Slug: "post-test-tool",
EndpointURL: "https://httpbin.org/post",
HTTPMethod: "POST",
AuthType: model.ToolAuthTypeNone,
RequestTemplate: "{\"message\": \"{{.message}}\"}",
Enabled: true,
}
s.Require().NoError(s.db.Create(postTool).Error)
@@ -205,4 +205,4 @@ func (s *CaptainCustomToolTestHandlerTestSuite) TestTestTool_带POST方法和参
func TestCaptainCustomToolTestHandlerSuite(t *testing.T) {
suite.Run(t, new(CaptainCustomToolTestHandlerTestSuite))
}
}
+3 -3
View File
@@ -96,7 +96,7 @@ func (h *CategoryHandler) Update(c *gin.Context) {
category, err := h.svc.Update(c.Request.Context(), uint(categoryID), &req)
if err != nil {
applogger.L().Errorf("Update category: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update category")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to update category")
return
}
@@ -114,7 +114,7 @@ func (h *CategoryHandler) Delete(c *gin.Context) {
if err := h.svc.Delete(c.Request.Context(), uint(categoryID)); err != nil {
applogger.L().Errorf("Delete category: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete category")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to delete category")
return
}
@@ -164,4 +164,4 @@ func (h *CategoryHandler) Reorder(c *gin.Context) {
}
response.NoContent(c)
}
}
@@ -200,13 +200,13 @@ func (s *CompanyHandlerTestSuite) TestCreate_ValidationError() {
// ========== Search ==========
func (s *CompanyHandlerTestSuite) TestSearch_NoResult() {
// Note: ILIKE is PostgreSQL-specific and doesn't work with SQLite.
// The handler will return a 500 error from the service layer due to
// SQLite not supporting ILIKE. This is expected in test environments.
// In production with PostgreSQL, this would return 200 with results.
w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/search?q=Nonexistent", s.accountID), nil)
// SQLite doesn't support ILIKE, so the handler returns 500
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
data := resp["data"].([]interface{})
assert.Len(s.T(), data, 0)
}
// ========== Get ==========
+7 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
)
// ContactHandler handles contact-related API endpoints.
// Reference: Chatwoot app/controllers/api/v1/contacts_controller.rb
type ContactHandler struct {
@@ -463,7 +464,7 @@ func (h *ContactHandler) CreateContactInbox(c *gin.Context) {
}
var req struct {
InboxID uint `json:"inbox_id" binding:"required"`
InboxID uint `json:"inbox_id" binding:"required"`
SourceID string `json:"source_id"`
}
if bindErr := c.ShouldBindJSON(&req); bindErr != nil {
@@ -522,6 +523,10 @@ func (h *ContactHandler) Active(c *gin.Context) {
perPage := getPageSize(c)
offset := (page - 1) * perPage
sort := c.DefaultQuery("sort", "")
if !h.svc.Ready() {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list active contacts"})
return
}
contacts, total, svcErr := h.svc.ListActive(c.Request.Context(), accountID, offset, perPage, sort)
if svcErr != nil {
@@ -628,6 +633,7 @@ func (h *ContactHandler) DeleteCustomAttributes(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "custom attributes deleted"})
}
// Merge two contacts into one. The base contact survives, mergee is deleted.
// POST /api/v1/accounts/:id/contacts/merge
// Reference: Chatwoot app/controllers/api/v1/accounts/actions/contact_merges_controller.rb
+10 -6
View File
@@ -44,8 +44,8 @@ func (h *CsatSurveyHandler) List(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"csat_survey_responses": responses,
"meta": gin.H{
"count": total,
"page": filter.Page,
"count": total,
"page": filter.Page,
"per_page": filter.PageSize,
},
})
@@ -96,9 +96,9 @@ func (h *CsatSurveyHandler) UpdateReviewNotes(c *gin.Context) {
}
response.OK(c, gin.H{
"id": id,
"csat_review_notes": body.CsatReviewNotes,
"updated_by_id": userID,
"id": id,
"csat_review_notes": body.CsatReviewNotes,
"updated_by_id": userID,
})
}
@@ -120,6 +120,10 @@ func (h *CsatSurveyHandler) Update(c *gin.Context) {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to update csat survey response")
return
}
resp, svcErr := h.svc.Update(c.Request.Context(), id, body.Rating, body.FeedbackMessage, body.CsatReviewNotes)
if svcErr != nil {
@@ -325,4 +329,4 @@ func sanitizeCSV(s string) string {
s = strings.ReplaceAll(s, "\"", "\"\"")
s = strings.ReplaceAll(s, "\n", " ")
return s
}
}
@@ -154,6 +154,10 @@ func (h *DraftMessageHandler) Search(c *gin.Context) {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "search query 'q' is required")
return
}
if !h.draftSvc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to search draft messages")
return
}
drafts, svcErr := h.draftSvc.Search(c.Request.Context(), accountID, query)
if svcErr != nil {
@@ -172,6 +176,10 @@ func (h *DraftMessageHandler) Count(c *gin.Context) {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
if !h.draftSvc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to count draft messages")
return
}
count, svcErr := h.draftSvc.Count(c.Request.Context(), accountID)
if svcErr != nil {
@@ -180,4 +188,4 @@ func (h *DraftMessageHandler) Count(c *gin.Context) {
}
response.OK(c, gin.H{"count": count})
}
}
+28 -1
View File
@@ -16,6 +16,33 @@ func parseUintParam(c *gin.Context, param string) (uint, error) {
return uint(n), nil
}
func parseUintAnyParam(c *gin.Context, params ...string) (uint, error) {
var lastErr error
for _, param := range params {
if c.Param(param) == "" {
continue
}
id, err := parseUintParam(c, param)
if err == nil && id != 0 {
return id, nil
}
return 0, err
}
return 0, lastErr
}
// parseAccountIDParam accepts both Chatwoot-style :account_id and older local
// tests/routes that still mount the account parameter as :id.
func parseAccountIDParam(c *gin.Context) uint {
if id, err := parseUintParam(c, "account_id"); err == nil && id != 0 {
return id
}
if id, err := parseUintParam(c, "id"); err == nil && id != 0 {
return id
}
return 0
}
// getAccountID extracts account ID from the request.
// Priority: URL param account_id > X-Account-ID header > JWT claims account_id
func getAccountID(c *gin.Context) uint {
@@ -127,4 +154,4 @@ func getPageSize(c *gin.Context) int {
return 100
}
return n
}
}
@@ -15,13 +15,13 @@ func setupInboxAgentBotRouter(handler *InboxHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(gin.Recovery())
r.GET("/api/v1/accounts/:inbox_id/inboxes/:inbox_id/agent_bot", handler.GetAgentBot)
r.DELETE("/api/v1/accounts/:inbox_id/inboxes/:inbox_id/avatar", handler.DeleteAvatar)
r.GET("/api/v1/accounts/:inbox_id/inboxes/:inbox_id/campaigns", handler.ListCampaigns)
r.GET("/api/v1/accounts/:account_id/inboxes/:inbox_id/agent_bot", handler.GetAgentBot)
r.DELETE("/api/v1/accounts/:account_id/inboxes/:inbox_id/avatar", handler.DeleteAvatar)
r.GET("/api/v1/accounts/:account_id/inboxes/:inbox_id/campaigns", handler.ListCampaigns)
return r
}
// --- GetAgentBot tests ---
// --- GetAgentBot tests ---
func TestInboxHandler_GetAgentBot_InvalidAccountID(t *testing.T) {
handler := NewInboxHandler(&service.InboxService{})
@@ -57,7 +57,7 @@ func TestInboxHandler_GetAgentBot_ValidIDs_ZeroService(t *testing.T) {
assert.True(t, w.Code == http.StatusUnprocessableEntity || w.Code == http.StatusOK)
}
// --- DeleteAvatar tests ---
// --- DeleteAvatar tests ---
func TestInboxHandler_DeleteAvatar_InvalidAccountID(t *testing.T) {
handler := NewInboxHandler(&service.InboxService{})
@@ -93,7 +93,7 @@ func TestInboxHandler_DeleteAvatar_ValidIDs_ZeroService(t *testing.T) {
assert.True(t, w.Code == http.StatusUnprocessableEntity || w.Code == http.StatusOK)
}
// --- ListCampaigns tests ---
// --- ListCampaigns tests ---
func TestInboxHandler_ListCampaigns_InvalidAccountID(t *testing.T) {
handler := NewInboxHandler(&service.InboxService{})
@@ -127,4 +127,4 @@ func TestInboxHandler_ListCampaigns_ValidIDs_ZeroService(t *testing.T) {
// Zero service will panic on method call; Recovery middleware catches it → 500
assert.True(t, w.Code == http.StatusUnprocessableEntity || w.Code == http.StatusOK)
}
}
+49 -15
View File
@@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/response"
)
// InboxHandler handles inbox-related API endpoints.
@@ -224,8 +225,8 @@ func (h *InboxHandler) Delete(c *gin.Context) {
// POST /api/v1/accounts/:id/inboxes/:inbox_id/set_agent_bot
// Reference: Chatwoot InboxesController#set_agent_bot
func (h *InboxHandler) SetAgentBot(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
@@ -241,6 +242,10 @@ func (h *InboxHandler) SetAgentBot(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body: " + err.Error()})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to set agent bot")
return
}
binding, svcErr := h.svc.SetAgentBot(c.Request.Context(), accountID, inboxID, req)
if svcErr != nil {
@@ -261,8 +266,8 @@ func (h *InboxHandler) SetAgentBot(c *gin.Context) {
// GET /api/v1/accounts/:id/inboxes/:inbox_id/health
// Reference: Chatwoot InboxesController#health
func (h *InboxHandler) Health(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
@@ -273,6 +278,11 @@ func (h *InboxHandler) Health(c *gin.Context) {
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to check inbox health")
return
}
result, svcErr := h.svc.Health(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
@@ -286,8 +296,8 @@ func (h *InboxHandler) Health(c *gin.Context) {
// POST /api/v1/accounts/:id/inboxes/:inbox_id/sync_templates
// Reference: Chatwoot InboxesController#sync_templates
func (h *InboxHandler) SyncTemplates(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
@@ -298,6 +308,11 @@ func (h *InboxHandler) SyncTemplates(c *gin.Context) {
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to sync templates")
return
}
templates, svcErr := h.svc.SyncTemplates(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
@@ -311,8 +326,8 @@ func (h *InboxHandler) SyncTemplates(c *gin.Context) {
// POST /api/v1/accounts/:id/inboxes/:inbox_id/register_webhook
// Reference: Chatwoot InboxesController#register_webhook
func (h *InboxHandler) RegisterWebhook(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
@@ -328,6 +343,10 @@ func (h *InboxHandler) RegisterWebhook(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body: " + err.Error()})
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to register webhook")
return
}
svcErr := h.svc.RegisterWebhook(c.Request.Context(), accountID, inboxID, req)
if svcErr != nil {
@@ -342,8 +361,8 @@ func (h *InboxHandler) RegisterWebhook(c *gin.Context) {
// GET /api/v1/accounts/:id/inboxes/:inbox_id/agent_bot
// Reference: Chatwoot InboxesController#agent_bot
func (h *InboxHandler) GetAgentBot(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
@@ -354,6 +373,11 @@ func (h *InboxHandler) GetAgentBot(c *gin.Context) {
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to get agent bot")
return
}
agentBot, svcErr := h.svc.GetAgentBot(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
@@ -372,8 +396,8 @@ func (h *InboxHandler) GetAgentBot(c *gin.Context) {
// DELETE /api/v1/accounts/:id/inboxes/:inbox_id/avatar
// Reference: Chatwoot InboxesController#destroy_avatar
func (h *InboxHandler) DeleteAvatar(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
@@ -384,6 +408,11 @@ func (h *InboxHandler) DeleteAvatar(c *gin.Context) {
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to delete inbox avatar")
return
}
inbox, svcErr := h.svc.DeleteAvatar(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
@@ -397,8 +426,8 @@ func (h *InboxHandler) DeleteAvatar(c *gin.Context) {
// GET /api/v1/accounts/:id/inboxes/:inbox_id/campaigns
// Reference: Chatwoot InboxesController#campaigns
func (h *InboxHandler) ListCampaigns(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
return
}
@@ -409,6 +438,11 @@ func (h *InboxHandler) ListCampaigns(c *gin.Context) {
return
}
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to list campaigns")
return
}
campaigns, svcErr := h.svc.ListCampaigns(c.Request.Context(), accountID, inboxID)
if svcErr != nil {
handleServiceError(c, svcErr)
@@ -441,4 +475,4 @@ func (h *InboxHandler) ResetSecret(c *gin.Context) {
}
c.JSON(http.StatusOK, inbox)
}
}
@@ -18,10 +18,10 @@ import (
func setupInboxMemberActionRouter(handler *InboxHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
r.POST("/api/v1/accounts/:inbox_id/inboxes/:inbox_id/set_agent_bot", handler.SetAgentBot)
r.GET("/api/v1/accounts/:inbox_id/inboxes/:inbox_id/health", handler.Health)
r.POST("/api/v1/accounts/:inbox_id/inboxes/:inbox_id/sync_templates", handler.SyncTemplates)
r.POST("/api/v1/accounts/:inbox_id/inboxes/:inbox_id/register_webhook", handler.RegisterWebhook)
r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/set_agent_bot", handler.SetAgentBot)
r.GET("/api/v1/accounts/:account_id/inboxes/:inbox_id/health", handler.Health)
r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/sync_templates", handler.SyncTemplates)
r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/register_webhook", handler.RegisterWebhook)
return r
}
@@ -149,4 +149,4 @@ func TestInboxRegisterWebhook_BadInboxID(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid inbox id")
}
}
@@ -71,7 +71,7 @@ func (s *InboxLimitHandlerTestSuite) TestCreate_BadRequest_EmptyBody() {
func (s *InboxLimitHandlerTestSuite) TestUpdate_BadRequest_InvalidID() {
r := gin.New()
r.PUT("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:inbox_id", s.handler.Update)
r.PUT("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:id", s.handler.Update)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/inbox_limits/abc", s.account.ID, s.inbox.ID), bytes.NewBufferString(`{"limit_count":10}`))
@@ -83,7 +83,7 @@ func (s *InboxLimitHandlerTestSuite) TestUpdate_BadRequest_InvalidID() {
func (s *InboxLimitHandlerTestSuite) TestDelete_BadRequest_InvalidID() {
r := gin.New()
r.DELETE("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:inbox_id", s.handler.Delete)
r.DELETE("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:id", s.handler.Delete)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/inbox_limits/abc", s.account.ID, s.inbox.ID), nil)
@@ -110,7 +110,7 @@ func (s *InboxLimitHandlerTestSuite) TestUpdate_Success() {
s.Require().NoError(s.db.Create(limit).Error)
r := gin.New()
r.PUT("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:inbox_id", s.handler.Update)
r.PUT("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:id", s.handler.Update)
w := httptest.NewRecorder()
body := `{"type":"max_conversations","value":200}`
@@ -126,11 +126,11 @@ func (s *InboxLimitHandlerTestSuite) TestDelete_Success() {
s.Require().NoError(s.db.Create(limit).Error)
r := gin.New()
r.DELETE("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:inbox_id", s.handler.Delete)
r.DELETE("/api/v1/accounts/:account_id/inboxes/:inbox_id/inbox_limits/:id", s.handler.Delete)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/inbox_limits/%d", s.account.ID, s.inbox.ID, limit.ID), nil)
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
}
}
@@ -2,6 +2,7 @@ package v1
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
@@ -26,6 +27,11 @@ func NewIntegrationHookHandler(svc *service.IntegrationHookService) *Integration
// ListApps retrieves all available integration apps.
// GET /api/v1/accounts/:account_id/integrations/apps
func (h *IntegrationHookHandler) ListApps(c *gin.Context) {
if !h.svc.Ready() {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to list integration apps")
return
}
apps, err := h.svc.ListApps(c.Request.Context())
if err != nil {
handleServiceError(c, err)
@@ -174,7 +180,11 @@ func (h *IntegrationHookHandler) ProcessHookEvent(c *gin.Context) {
}
if svcErr := h.svc.ProcessEvent(c.Request.Context(), id, eventData); svcErr != nil {
handleServiceError(c, svcErr)
if strings.Contains(strings.ToLower(svcErr.Error()), "not found") {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, svcErr.Error())
return
}
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, svcErr.Error())
return
}
response.OK(c, gin.H{"message": "event processed"})
+12 -2
View File
@@ -2,6 +2,7 @@ package v1
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
@@ -289,8 +290,17 @@ func (h *MessageHandler) Translate(c *gin.Context) {
result, svcErr := h.svc.Translate(c.Request.Context(), accountID, messageID, req)
if svcErr != nil {
handleServiceError(c, svcErr)
lower := strings.ToLower(svcErr.Error())
if strings.Contains(lower, "not found") {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, svcErr.Error())
return
}
if strings.Contains(lower, "required") || strings.Contains(lower, "invalid") {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, svcErr.Error())
return
}
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, svcErr.Error())
return
}
response.OK(c, result)
}
}
+3 -3
View File
@@ -68,7 +68,7 @@ func (h *MFAHandler) EnableMFA(c *gin.Context) {
// Check if MFA is already enabled
enabled, err := h.mfaService.IsMFAEnabled(userID)
if err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, err.Error())
return
}
if enabled {
@@ -79,7 +79,7 @@ func (h *MFAHandler) EnableMFA(c *gin.Context) {
// Generate new TOTP secret + QR URI
secret, qrURI, err := h.mfaService.GenerateTOTPSecret(userID)
if err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, err.Error())
return
}
@@ -202,4 +202,4 @@ func (h *MFAHandler) BackupCodes(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{"backup_codes": codes})
}
}
@@ -39,8 +39,8 @@ func NewPlatformAccountUserHandler(
// Reference: Chatwoot Platform::Api::V1::AccountUsersController#index
// Requires: Account must be within PlatformApp's permissible scope.
func (h *PlatformAccountUserHandler) Index(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID, err := parseUintAnyParam(c, "account_id", "id")
if err != nil || accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
}
@@ -68,8 +68,8 @@ func (h *PlatformAccountUserHandler) Index(c *gin.Context) {
// Reference: Chatwoot Platform::Api::V1::AccountUsersController#create
// Requires: Both account and user must be within PlatformApp's permissible scope.
func (h *PlatformAccountUserHandler) Create(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID, err := parseUintAnyParam(c, "account_id", "id")
if err != nil || accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
}
@@ -124,13 +124,13 @@ func (h *PlatformAccountUserHandler) Create(c *gin.Context) {
// Reference: Chatwoot Platform::Api::V1::AccountUsersController#destroy
// Requires: Account must be within PlatformApp's permissible scope.
func (h *PlatformAccountUserHandler) Destroy(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
if err != nil {
accountID, err := parseUintAnyParam(c, "account_id", "id")
if err != nil || accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
}
userID, err := parseUintParam(c, "id")
userID, err := parseUintParam(c, "user_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID")
return
@@ -151,4 +151,4 @@ func (h *PlatformAccountUserHandler) Destroy(c *gin.Context) {
}
response.NoContent(c)
}
}
+8 -8
View File
@@ -76,8 +76,8 @@ func setupPlatformTokenTestE2E(t *testing.T) (*gin.Engine, *repository.Permissib
platformGroup.GET("/users", platformUser.List)
platformGroup.GET("/users/:id", platformUser.Show)
platformGroup.POST("/users", platformUser.Create)
platformGroup.POST("/users/:agent_bot_id/login", platformUser.Login)
platformGroup.POST("/users/:agent_bot_id/token", platformUser.Token)
platformGroup.POST("/users/:id/login", platformUser.Login)
platformGroup.POST("/users/:id/token", platformUser.Token)
platformGroup.PATCH("/users/:id", platformUser.Update)
platformGroup.DELETE("/users/:id", platformUser.Destroy)
@@ -92,12 +92,12 @@ func setupPlatformTokenTestE2E(t *testing.T) (*gin.Engine, *repository.Permissib
platformGroup.POST("/agent_bots", platformAgentBot.Create)
platformGroup.PUT("/agent_bots/:id", platformAgentBot.Update)
platformGroup.DELETE("/agent_bots/:id", platformAgentBot.Destroy)
platformGroup.POST("/agent_bots/:agent_bot_id/delete_avatar", platformAgentBot.DeleteAvatar)
platformGroup.POST("/agent_bots/:id/delete_avatar", platformAgentBot.DeleteAvatar)
// Gin wildcard constraint: nested routes under accounts/:id must use :id (not :account_id)
platformGroup.GET("/accounts/:agent_bot_id/account_users", platformAccountUser.Index)
platformGroup.POST("/accounts/:agent_bot_id/account_users", platformAccountUser.Create)
platformGroup.DELETE("/accounts/:agent_bot_id/account_users/:user_id", platformAccountUser.Destroy)
// Gin wildcard constraint: nested routes under accounts/:id must reuse :id.
platformGroup.GET("/accounts/:id/account_users", platformAccountUser.Index)
platformGroup.POST("/accounts/:id/account_users", platformAccountUser.Create)
platformGroup.DELETE("/accounts/:id/account_users/:user_id", platformAccountUser.Destroy)
return engine, permissibleRepo, userRepo, accountRepo
}
@@ -443,4 +443,4 @@ func TestPlatformAccountUserE2E_Index(t *testing.T) {
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
}
+5 -5
View File
@@ -122,7 +122,7 @@ func (h *SearchHandler) SearchConversations(c *gin.Context) {
results, total, svcErr := h.svc.SearchConversations(c.Request.Context(), accountID, query, &filter)
if svcErr != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "conversation search failed")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "conversation search failed")
return
}
@@ -175,7 +175,7 @@ func (h *SearchHandler) SearchMessages(c *gin.Context) {
results, total, svcErr := h.svc.SearchMessages(c.Request.Context(), accountID, query, &filter)
if svcErr != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "message search failed")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "message search failed")
return
}
@@ -222,7 +222,7 @@ func (h *SearchHandler) SearchContacts(c *gin.Context) {
results, total, svcErr := h.svc.SearchContacts(c.Request.Context(), accountID, query, &filter)
if svcErr != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "contact search failed")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "contact search failed")
return
}
@@ -272,7 +272,7 @@ func (h *SearchHandler) SearchArticles(c *gin.Context) {
results, total, svcErr := h.svc.SearchArticles(c.Request.Context(), accountID, query, &filter)
if svcErr != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "article search failed")
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "article search failed")
return
}
@@ -280,4 +280,4 @@ func (h *SearchHandler) SearchArticles(c *gin.Context) {
"results": results,
"by_type": gin.H{"article": total},
}, filter.Page, filter.PerPage, total)
}
}
@@ -81,9 +81,9 @@ func setupSlaPolicyTestRouter(handler *SlaPolicyHandler) *gin.Engine {
rg := r.Group("/api/v1/accounts/:account_id")
rg.GET("/sla_policies", handler.List)
rg.POST("/sla_policies", handler.Create)
rg.GET("/sla_policies/:inbox_id", handler.Get)
rg.PUT("/sla_policies/:inbox_id", handler.Update)
rg.DELETE("/sla_policies/:inbox_id", handler.Delete)
rg.GET("/sla_policies/:id", handler.Get)
rg.PUT("/sla_policies/:id", handler.Update)
rg.DELETE("/sla_policies/:id", handler.Delete)
rg.GET("/sla_policies/:id/inboxes", handler.ListInboxes)
rg.POST("/sla_policies/:id/inboxes", handler.AddInbox)
rg.DELETE("/sla_policies/:id/inboxes/:inbox_id", handler.RemoveInbox)
@@ -144,9 +144,9 @@ func TestSlaPolicyHandler_Create_Success(t *testing.T) {
"sla_policy": map[string]interface{}{
"name": "New SLA",
"description": "desc",
"first_response_time_threshold": 30,
"next_response_time_threshold": 60,
"resolution_time_threshold": 480,
"first_response_time_threshold": 30,
"next_response_time_threshold": 60,
"resolution_time_threshold": 480,
},
}
jsonBody, _ := json.Marshal(body)
@@ -189,9 +189,9 @@ func TestSlaPolicyHandler_Create_NoAccountID(t *testing.T) {
body := map[string]interface{}{
"sla_policy": map[string]interface{}{
"name": "New SLA",
"first_response_time_threshold": 30,
"next_response_time_threshold": 60,
"resolution_time_threshold": 480,
"first_response_time_threshold": 30,
"next_response_time_threshold": 60,
"resolution_time_threshold": 480,
},
}
jsonBody, _ := json.Marshal(body)
@@ -260,7 +260,7 @@ func TestSlaPolicyHandler_Update_Success(t *testing.T) {
body := map[string]interface{}{
"sla_policy": map[string]interface{}{
"name": "Updated Name",
"first_response_time_threshold": 45,
"first_response_time_threshold": 45,
},
}
jsonBody, _ := json.Marshal(body)
@@ -278,7 +278,7 @@ func TestSlaPolicyHandler_Update_NoAccountID(t *testing.T) {
r := gin.New()
handler, _ := setupSlaPolicyHandlerTest(t)
rg := r.Group("/api/v1/accounts")
rg.PUT("/sla_policies/:inbox_id", handler.Update)
rg.PUT("/sla_policies/:id", handler.Update)
body := map[string]interface{}{
"sla_policy": map[string]interface{}{
@@ -325,7 +325,7 @@ func TestSlaPolicyHandler_Delete_NoAccountID(t *testing.T) {
r := gin.New()
handler, _ := setupSlaPolicyHandlerTest(t)
rg := r.Group("/api/v1/accounts")
rg.DELETE("/sla_policies/:inbox_id", handler.Delete)
rg.DELETE("/sla_policies/:id", handler.Delete)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/sla_policies/1", nil)