chore: stabilize chatwoot parity baseline
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user