package v1 import ( "encoding/json" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/response" ) // AgentCapacityHandler handles AgentCapacityPolicy CRUD operations. // Reference: Chatwoot enterprise/app/controllers/api/v1/agent_capacity_policies_controller.rb type AgentCapacityHandler struct { svc *service.AgentCapacityPolicyService auditSvc *service.AuditService } // NewAgentCapacityHandler creates a new AgentCapacityPolicy handler. func NewAgentCapacityHandler(svc *service.AgentCapacityPolicyService) *AgentCapacityHandler { return &AgentCapacityHandler{svc: svc} } func (h *AgentCapacityHandler) WithAuditService(auditSvc *service.AuditService) *AgentCapacityHandler { h.auditSvc = auditSvc return h } // List returns all agent capacity policies for an account. // GET /api/v1/accounts/:account_id/agent_capacity_policies func (h *AgentCapacityHandler) List(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } policies, _, err := h.svc.List(c.Request.Context(), accountID, 1, 1000) if err != nil { applogger.L().Errorf("List agent capacity policies for account %d: %v", accountID, err) handleServiceError(c, err) return } payload := make([]gin.H, 0, len(policies)) for i := range policies { payload = append(payload, serializeAgentCapacityPolicy(&policies[i])) } c.JSON(http.StatusOK, payload) } // Create creates a new agent capacity policy for an account. // POST /api/v1/accounts/:account_id/agent_capacity_policies // Request body uses Chatwoot-style wrapper: { "agent_capacity_policy": { "name": "...", ... } } func (h *AgentCapacityHandler) Create(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } req, err := bindAgentCapacityPolicyCreate(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } policy, err := h.svc.Create(c.Request.Context(), accountID, req) if err != nil { applogger.L().Errorf("Create agent capacity policy for account %d: %v", accountID, err) handleAgentCapacityError(c, err) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "AgentCapacityPolicy", AuditableID: policy.ID, Action: "create", AuditedChanges: serializeAgentCapacityPolicy(policy), }) c.JSON(http.StatusOK, serializeAgentCapacityPolicy(policy)) } // Get returns a single agent capacity policy by ID. // GET /api/v1/accounts/:account_id/agent_capacity_policies/:id func (h *AgentCapacityHandler) Get(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } policy, svcErr := h.svc.GetByID(c.Request.Context(), id, accountID) if svcErr != nil { applogger.L().Errorf("Get agent capacity policy %d for account %d: %v", id, accountID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeAgentCapacityPolicy(policy)) } // Update updates an existing agent capacity policy. // PUT /api/v1/accounts/:account_id/agent_capacity_policies/:id func (h *AgentCapacityHandler) Update(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } req, err := bindAgentCapacityPolicyUpdate(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } policy, svcErr := h.svc.Update(c.Request.Context(), id, accountID, req) if svcErr != nil { applogger.L().Errorf("Update agent capacity policy %d for account %d: %v", id, accountID, svcErr) handleAgentCapacityError(c, svcErr) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "AgentCapacityPolicy", AuditableID: policy.ID, Action: "update", AuditedChanges: serializeAgentCapacityPolicy(policy), }) c.JSON(http.StatusOK, serializeAgentCapacityPolicy(policy)) } // Delete deletes an agent capacity policy. // DELETE /api/v1/accounts/:account_id/agent_capacity_policies/:id func (h *AgentCapacityHandler) Delete(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } if svcErr := h.svc.Delete(c.Request.Context(), id, accountID); svcErr != nil { applogger.L().Errorf("Delete agent capacity policy %d for account %d: %v", id, accountID, svcErr) handleServiceError(c, svcErr) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "AgentCapacityPolicy", AuditableID: id, Action: "destroy", AuditedChanges: gin.H{"id": id}, }) c.Status(http.StatusOK) } func (h *AgentCapacityHandler) CreateInboxLimit(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } policyID, err := parseUintAnyParam(c, "agent_capacity_policy_id", "id") if err != nil || policyID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } var req service.CreateInboxCapacityLimitRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } limit, svcErr := h.svc.CreateInboxCapacityLimit(c.Request.Context(), policyID, accountID, req) if svcErr != nil { handleAgentCapacityError(c, svcErr) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "InboxCapacityLimit", AuditableID: limit.ID, Action: "create", AuditedChanges: serializeInboxCapacityLimit(limit, false), }) c.JSON(http.StatusOK, serializeInboxCapacityLimit(limit, false)) } func (h *AgentCapacityHandler) UpdateInboxLimit(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } policyID, err := parseUintAnyParam(c, "agent_capacity_policy_id", "id") if err != nil || policyID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } limitID, err := parseUintAnyParam(c, "limit_id", "id") if err != nil || limitID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid limit id") return } var req service.UpdateInboxCapacityLimitRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } limit, svcErr := h.svc.UpdateInboxCapacityLimit(c.Request.Context(), policyID, accountID, limitID, req) if svcErr != nil { handleAgentCapacityError(c, svcErr) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "InboxCapacityLimit", AuditableID: limit.ID, Action: "update", AuditedChanges: serializeInboxCapacityLimit(limit, true), }) c.JSON(http.StatusOK, serializeInboxCapacityLimit(limit, true)) } func (h *AgentCapacityHandler) DeleteInboxLimit(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } policyID, err := parseUintAnyParam(c, "agent_capacity_policy_id", "id") if err != nil || policyID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } limitID, err := parseUintAnyParam(c, "limit_id", "id") if err != nil || limitID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid limit id") return } if svcErr := h.svc.DeleteInboxCapacityLimit(c.Request.Context(), policyID, accountID, limitID); svcErr != nil { handleAgentCapacityError(c, svcErr) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "InboxCapacityLimit", AuditableID: limitID, Action: "destroy", AuditedChanges: gin.H{"id": limitID, "agent_capacity_policy_id": policyID}, }) c.Status(http.StatusNoContent) } func (h *AgentCapacityHandler) ListUsers(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } policyID, err := parseUintAnyParam(c, "agent_capacity_policy_id", "id") if err != nil || policyID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } users, svcErr := h.svc.ListUsers(c.Request.Context(), policyID, accountID) if svcErr != nil { handleAgentCapacityError(c, svcErr) return } payload := make([]gin.H, 0, len(users)) for i := range users { payload = append(payload, serializeAgentCapacityUser(&users[i], accountID)) } c.JSON(http.StatusOK, payload) } func (h *AgentCapacityHandler) CreateUser(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } policyID, err := parseUintAnyParam(c, "agent_capacity_policy_id", "id") if err != nil || policyID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } var req service.AssignCapacityPolicyUserRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } user, svcErr := h.svc.AssignUser(c.Request.Context(), policyID, accountID, req) if svcErr != nil { handleAgentCapacityError(c, svcErr) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "AgentCapacityPolicyUser", AuditableID: user.ID, Action: "create", AuditedChanges: gin.H{ "agent_capacity_policy_id": policyID, "user_id": user.ID, }, }) c.JSON(http.StatusOK, serializeAgentCapacityUser(user, accountID)) } func (h *AgentCapacityHandler) DeleteUser(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } policyID, err := parseUintAnyParam(c, "agent_capacity_policy_id", "id") if err != nil || policyID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") return } userID, err := parseUintAnyParam(c, "user_id", "id") if err != nil || userID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user id") return } if svcErr := h.svc.RemoveUser(c.Request.Context(), policyID, accountID, userID); svcErr != nil { handleAgentCapacityError(c, svcErr) return } recordAuditMutation(c, h.auditSvc, auditMutation{ AccountID: accountID, AuditableType: "AgentCapacityPolicyUser", AuditableID: userID, Action: "destroy", AuditedChanges: gin.H{ "agent_capacity_policy_id": policyID, "user_id": userID, }, }) c.Status(http.StatusOK) } // RegisterAgentCapacityRoutes registers agent capacity policy routes on a gin.RouterGroup. func RegisterAgentCapacityRoutes(rg *gin.RouterGroup, h *AgentCapacityHandler) { policies := rg.Group("/agent_capacity_policies") { policies.GET("/", h.List) policies.POST("/", h.Create) policies.GET("/:id", h.Get) policies.PUT("/:id", h.Update) policies.PATCH("/:id", h.Update) policies.DELETE("/:id", h.Delete) policies.GET("/:id/users", h.ListUsers) policies.POST("/:id/users", h.CreateUser) policies.DELETE("/:id/users/:user_id", h.DeleteUser) policies.POST("/:id/inbox_limits", h.CreateInboxLimit) policies.PUT("/:id/inbox_limits/:limit_id", h.UpdateInboxLimit) policies.PATCH("/:id/inbox_limits/:limit_id", h.UpdateInboxLimit) policies.DELETE("/:id/inbox_limits/:limit_id", h.DeleteInboxLimit) } } func bindAgentCapacityPolicyCreate(c *gin.Context) (service.CreateAgentCapacityPolicyRequest, error) { var raw map[string]json.RawMessage if err := c.ShouldBindJSON(&raw); err != nil { return service.CreateAgentCapacityPolicyRequest{}, err } var req service.CreateAgentCapacityPolicyRequest if wrapped, ok := raw["agent_capacity_policy"]; ok { return req, json.Unmarshal(wrapped, &req) } buf, err := json.Marshal(raw) if err != nil { return req, err } return req, json.Unmarshal(buf, &req) } func bindAgentCapacityPolicyUpdate(c *gin.Context) (service.UpdateAgentCapacityPolicyRequest, error) { var raw map[string]json.RawMessage if err := c.ShouldBindJSON(&raw); err != nil { return service.UpdateAgentCapacityPolicyRequest{}, err } var req service.UpdateAgentCapacityPolicyRequest if wrapped, ok := raw["agent_capacity_policy"]; ok { return req, json.Unmarshal(wrapped, &req) } buf, err := json.Marshal(raw) if err != nil { return req, err } return req, json.Unmarshal(buf, &req) } func serializeAgentCapacityPolicy(policy *model.AgentCapacityPolicy) gin.H { limits := make([]gin.H, 0, len(policy.InboxCapacityLimits)) for i := range policy.InboxCapacityLimits { limit := policy.InboxCapacityLimits[i] limits = append(limits, gin.H{ "id": limit.ID, "inbox_id": limit.InboxID, "conversation_limit": limit.ConversationLimit, }) } return gin.H{ "id": policy.ID, "name": policy.Name, "description": policy.Description, "exclusion_rules": policy.ExclusionRules, "created_at": policy.CreatedAt.Unix(), "updated_at": policy.UpdatedAt.Unix(), "account_id": policy.AccountID, "assigned_agent_count": len(policy.AccountUsers), "inbox_capacity_limits": limits, } } func serializeInboxCapacityLimit(limit *model.InboxCapacityLimit, includeInboxName bool) gin.H { payload := gin.H{ "id": limit.ID, "inbox_id": limit.InboxID, "agent_capacity_policy_id": limit.AgentCapacityPolicyID, "conversation_limit": limit.ConversationLimit, "created_at": limit.CreatedAt.Unix(), "updated_at": limit.UpdatedAt.Unix(), } if includeInboxName { payload["inbox_name"] = limit.Inbox.Name } return payload } func serializeAgentCapacityUser(user *model.User, accountID uint) gin.H { availableName := user.DisplayName if availableName == "" { availableName = user.Name } if availableName == "" { availableName = user.Email } return gin.H{ "access_token": nil, "account_id": accountID, "available_name": availableName, "avatar_url": user.AvatarURL, "confirmed": user.ConfirmedAt != nil, "display_name": user.DisplayName, "message_signature": user.MessageSignature, "email": user.Email, "id": user.ID, "inviter_id": nil, "name": user.Name, "provider": user.Provider, "pubsub_token": user.PubsubToken, "role": user.Role, "ui_settings": user.UISettings, "uid": user.UID, "type": user.Type, } } func handleAgentCapacityError(c *gin.Context, err error) { if err == nil { return } lower := strings.ToLower(err.Error()) if strings.Contains(lower, "already been assigned") || strings.Contains(lower, "greater than or equal") { response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, err.Error()) return } handleServiceError(c, err) }