feat(capacity): align chatwoot inbox capacity limits
This commit is contained in:
@@ -186,6 +186,18 @@ var criticalRoutes = []route{
|
||||
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/sla_policies/:id", Controller: "api/v1/accounts/sla_policies#destroy", Source: "routes.rb:121"},
|
||||
{Method: "GET", Path: "/api/v1/accounts/:account_id/custom_roles/", Controller: "api/v1/accounts/custom_roles#index", Source: "routes.rb:122"},
|
||||
{Method: "GET", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/", Controller: "api/v1/accounts/agent_capacity_policies#index", Source: "routes.rb:123"},
|
||||
{Method: "POST", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/", Controller: "api/v1/accounts/agent_capacity_policies#create", Source: "routes.rb:123"},
|
||||
{Method: "GET", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:id", Controller: "api/v1/accounts/agent_capacity_policies#show", Source: "routes.rb:123"},
|
||||
{Method: "PUT", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:id", Controller: "api/v1/accounts/agent_capacity_policies#update", Source: "routes.rb:123"},
|
||||
{Method: "PATCH", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:id", Controller: "api/v1/accounts/agent_capacity_policies#update", Source: "routes.rb:123"},
|
||||
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:id", Controller: "api/v1/accounts/agent_capacity_policies#destroy", Source: "routes.rb:123"},
|
||||
{Method: "GET", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users", Controller: "api/v1/accounts/agent_capacity_policies/users#index", Source: "routes.rb:125"},
|
||||
{Method: "POST", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users", Controller: "api/v1/accounts/agent_capacity_policies/users#create", Source: "routes.rb:125"},
|
||||
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users/:id", Controller: "api/v1/accounts/agent_capacity_policies/users#destroy", Source: "routes.rb:125"},
|
||||
{Method: "POST", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits", Controller: "api/v1/accounts/agent_capacity_policies/inbox_limits#create", Source: "routes.rb:126"},
|
||||
{Method: "PUT", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id", Controller: "api/v1/accounts/agent_capacity_policies/inbox_limits#update", Source: "routes.rb:126"},
|
||||
{Method: "PATCH", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id", Controller: "api/v1/accounts/agent_capacity_policies/inbox_limits#update", Source: "routes.rb:126"},
|
||||
{Method: "DELETE", Path: "/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id", Controller: "api/v1/accounts/agent_capacity_policies/inbox_limits#destroy", Source: "routes.rb:126"},
|
||||
{Method: "GET", Path: "/api/v1/accounts/:account_id/csat_survey_responses/", Controller: "api/v1/accounts/csat_survey_responses#index", Source: "routes.rb:219"},
|
||||
{Method: "GET", Path: "/api/v1/accounts/:account_id/csat_survey_responses/metrics", Controller: "api/v1/accounts/csat_survey_responses#metrics", Source: "routes.rb:221"},
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ DELETE /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/avatar
|
||||
DELETE /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/bot_rules/:rule_id
|
||||
DELETE /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:trigger_config_id
|
||||
DELETE /api/v1/accounts/:account_id/agent_capacity_policies/:id
|
||||
DELETE /api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id
|
||||
DELETE /api/v1/accounts/:account_id/agent_capacity_policies/:id/users/:user_id
|
||||
DELETE /api/v1/accounts/:account_id/agents/:agent_id
|
||||
DELETE /api/v1/accounts/:account_id/assignment_policies/:policy_id
|
||||
DELETE /api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes/:inbox_id
|
||||
@@ -123,6 +125,7 @@ GET /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/
|
||||
GET /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:trigger_config_id
|
||||
GET /api/v1/accounts/:account_id/agent_capacity_policies/
|
||||
GET /api/v1/accounts/:account_id/agent_capacity_policies/:id
|
||||
GET /api/v1/accounts/:account_id/agent_capacity_policies/:id/users
|
||||
GET /api/v1/accounts/:account_id/agents
|
||||
GET /api/v1/accounts/:account_id/agents/:agent_id
|
||||
GET /api/v1/accounts/:account_id/applied_slas/download
|
||||
@@ -439,6 +442,8 @@ GET /widget/widget/:website_token/theme_config
|
||||
GET /widget/widget/:website_token/uploads/:upload_uuid
|
||||
GET /ws
|
||||
PATCH /api/v1/accounts/:account_id/agent_bot_inboxes/:agent_bot_inbox_id/status
|
||||
PATCH /api/v1/accounts/:account_id/agent_capacity_policies/:id
|
||||
PATCH /api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id
|
||||
PATCH /api/v1/accounts/:account_id/channels/facebook_channel/:fb_id
|
||||
PATCH /api/v1/accounts/:account_id/companies/:company_id
|
||||
PATCH /api/v1/accounts/:account_id/contacts/:contact_id
|
||||
@@ -488,6 +493,8 @@ POST /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/reset_secret
|
||||
POST /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/reset_token
|
||||
POST /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/
|
||||
POST /api/v1/accounts/:account_id/agent_capacity_policies/
|
||||
POST /api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits
|
||||
POST /api/v1/accounts/:account_id/agent_capacity_policies/:id/users
|
||||
POST /api/v1/accounts/:account_id/agents
|
||||
POST /api/v1/accounts/:account_id/agents/bulk_assign
|
||||
POST /api/v1/accounts/:account_id/agents/bulk_create
|
||||
@@ -751,6 +758,7 @@ PUT /api/v1/accounts/:account_id/agent_bots/:agent_bot_id
|
||||
PUT /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/bot_rules/:rule_id
|
||||
PUT /api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:trigger_config_id
|
||||
PUT /api/v1/accounts/:account_id/agent_capacity_policies/:id
|
||||
PUT /api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id
|
||||
PUT /api/v1/accounts/:account_id/agents/:agent_id
|
||||
PUT /api/v1/accounts/:account_id/assignment_policies/:policy_id
|
||||
PUT /api/v1/accounts/:account_id/assignment_policies_v2/:id
|
||||
@@ -819,4 +827,4 @@ PUT /public/api/v1/csat_survey/:id
|
||||
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id
|
||||
PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id
|
||||
PUT /widget/direct_uploads/:upload_uuid
|
||||
TOTAL: 821
|
||||
TOTAL: 829
|
||||
|
||||
@@ -7,7 +7,7 @@ Generated from:
|
||||
|
||||
This report covers tracked frontend-critical Chatwoot routes from `reference/chatwoot/config/routes.rb`, including API v1 account routes, Captain/Copilot, assignment policies, widget/public APIs, and API v2 reports. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`.
|
||||
|
||||
Summary: 262 exact, 0 method-compatible, 0 parameter-compatible, 0 missing out of 262 tracked critical routes.
|
||||
Summary: 267 exact, 0 method-compatible, 7 parameter-compatible, 0 missing out of 274 tracked critical routes.
|
||||
|
||||
## Missing Critical Routes
|
||||
|
||||
@@ -29,12 +29,19 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
|
||||
| Method | Chatwoot Path | GoChat Match | Controller | Source | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| - | - | - | - | - | none |
|
||||
| DELETE | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id` | `api/v1/accounts/agent_capacity_policies/inbox_limits#destroy` | `routes.rb:126` | parameter-compatible |
|
||||
| DELETE | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/users/:user_id` | `api/v1/accounts/agent_capacity_policies/users#destroy` | `routes.rb:125` | parameter-compatible |
|
||||
| GET | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/users` | `api/v1/accounts/agent_capacity_policies/users#index` | `routes.rb:125` | parameter-compatible |
|
||||
| PATCH | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id` | `api/v1/accounts/agent_capacity_policies/inbox_limits#update` | `routes.rb:126` | parameter-compatible |
|
||||
| POST | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits` | `api/v1/accounts/agent_capacity_policies/inbox_limits#create` | `routes.rb:126` | parameter-compatible |
|
||||
| POST | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/users` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/users` | `api/v1/accounts/agent_capacity_policies/users#create` | `routes.rb:125` | parameter-compatible |
|
||||
| PUT | `/api/v1/accounts/:account_id/agent_capacity_policies/:agent_capacity_policy_id/inbox_limits/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id/inbox_limits/:limit_id` | `api/v1/accounts/agent_capacity_policies/inbox_limits#update` | `routes.rb:126` | parameter-compatible |
|
||||
|
||||
## Exact Critical Routes
|
||||
|
||||
| Method | Chatwoot Path | GoChat Match | Controller | Source | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| DELETE | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `api/v1/accounts/agent_capacity_policies#destroy` | `routes.rb:123` | exact |
|
||||
| DELETE | `/api/v1/accounts/:account_id/agents/:agent_id` | `/api/v1/accounts/:account_id/agents/:agent_id` | `api/v1/accounts/agents#destroy` | `routes.rb:59` | exact |
|
||||
| DELETE | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `api/v1/accounts/assignment_policies#destroy` | `routes.rb:306` | exact |
|
||||
| DELETE | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes/:inbox_id` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id/inboxes/:inbox_id` | `api/v1/accounts/assignment_policies/inboxes#destroy` | `routes.rb:307` | exact |
|
||||
@@ -68,6 +75,7 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
| DELETE | `/api/v1/widget/labels/:label_id` | `/api/v1/widget/labels/:label_id` | `api/v1/widget/labels#destroy` | `routes.rb:464` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id` | `/api/v1/accounts/:account_id` | `api/v1/accounts#show` | `routes.rb:47` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/agent_capacity_policies/` | `/api/v1/accounts/:account_id/agent_capacity_policies/` | `api/v1/accounts/agent_capacity_policies#index` | `routes.rb:123` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `api/v1/accounts/agent_capacity_policies#show` | `routes.rb:123` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/agents` | `/api/v1/accounts/:account_id/agents` | `api/v1/accounts/agents#index` | `routes.rb:59` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/assignable_agents` | `/api/v1/accounts/:account_id/assignable_agents` | `api/v1/accounts/assignable_agents#index` | `routes.rb:104` | exact |
|
||||
| GET | `/api/v1/accounts/:account_id/assignment_policies/` | `/api/v1/accounts/:account_id/assignment_policies/` | `api/v1/accounts/assignment_policies#index` | `routes.rb:306` | exact |
|
||||
@@ -179,11 +187,13 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
| GET | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations` | `public/api/v1/inboxes/conversations#index` | `routes.rb:573` | exact |
|
||||
| GET | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id` | `public/api/v1/inboxes/conversations#show` | `routes.rb:573` | exact |
|
||||
| GET | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages` | `public/api/v1/inboxes/messages#index` | `routes.rb:580` | exact |
|
||||
| PATCH | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `api/v1/accounts/agent_capacity_policies#update` | `routes.rb:123` | exact |
|
||||
| PATCH | `/api/v1/accounts/:account_id/inbox_members/` | `/api/v1/accounts/:account_id/inbox_members/` | `api/v1/accounts/inbox_members#update` | `routes.rb:278` | exact |
|
||||
| PATCH | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `/api/v1/accounts/:account_id/teams/:team_id/team_members/` | `api/v1/accounts/teams/team_members#update` | `routes.rb:300` | exact |
|
||||
| PATCH | `/api/v1/widget/contact/set_user` | `/api/v1/widget/contact/set_user` | `api/v1/widget/contact#set_user` | `routes.rb:461` | exact |
|
||||
| POST | `/api/v1/accounts/` | `/api/v1/accounts/` | `api/v1/accounts#create` | `routes.rb:47` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/actions/contact_merge` | `/api/v1/accounts/:account_id/actions/contact_merge` | `api/v1/accounts/actions/contact_merges#create` | `routes.rb:55` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/agent_capacity_policies/` | `/api/v1/accounts/:account_id/agent_capacity_policies/` | `api/v1/accounts/agent_capacity_policies#create` | `routes.rb:123` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/agents` | `/api/v1/accounts/:account_id/agents` | `api/v1/accounts/agents#create` | `routes.rb:59` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/agents/bulk_create` | `/api/v1/accounts/:account_id/agents/bulk_create` | `api/v1/accounts/agents#bulk_create` | `routes.rb:60` | exact |
|
||||
| POST | `/api/v1/accounts/:account_id/assignment_policies/` | `/api/v1/accounts/:account_id/assignment_policies/` | `api/v1/accounts/assignment_policies#create` | `routes.rb:306` | exact |
|
||||
@@ -270,6 +280,7 @@ These routes exist with equivalent method and path shape but different parameter
|
||||
| POST | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_typing` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/toggle_typing` | `public/api/v1/inboxes/conversations#toggle_typing` | `routes.rb:576` | exact |
|
||||
| POST | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/update_last_seen` | `/public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/update_last_seen` | `public/api/v1/inboxes/conversations#update_last_seen` | `routes.rb:577` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id` | `/api/v1/accounts/:account_id` | `api/v1/accounts#update` | `routes.rb:47` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `/api/v1/accounts/:account_id/agent_capacity_policies/:id` | `api/v1/accounts/agent_capacity_policies#update` | `routes.rb:123` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/agents/:agent_id` | `/api/v1/accounts/:account_id/agents/:agent_id` | `api/v1/accounts/agents#update` | `routes.rb:59` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `/api/v1/accounts/:account_id/assignment_policies/:policy_id` | `api/v1/accounts/assignment_policies#update` | `routes.rb:306` | exact |
|
||||
| PUT | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `/api/v1/accounts/:account_id/automation_rules/:automation_id` | `api/v1/accounts/automation_rules#update` | `routes.rb:115` | exact |
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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/pagination"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
)
|
||||
|
||||
@@ -31,15 +33,18 @@ func (h *AgentCapacityHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pg := pagination.Parse(c)
|
||||
policies, total, err := h.svc.List(c.Request.Context(), accountID, pg.Page, pg.PerPage)
|
||||
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
|
||||
}
|
||||
|
||||
response.OKWithMeta(c, policies, pg.Page, pg.PerPage, total)
|
||||
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.
|
||||
@@ -52,22 +57,20 @@ func (h *AgentCapacityHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var wrapper struct {
|
||||
AgentCapacityPolicy service.CreateAgentCapacityPolicyRequest `json:"agent_capacity_policy"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&wrapper); err != nil {
|
||||
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, wrapper.AgentCapacityPolicy)
|
||||
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)
|
||||
handleServiceError(c, err)
|
||||
handleAgentCapacityError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, policy)
|
||||
c.JSON(http.StatusOK, serializeAgentCapacityPolicy(policy))
|
||||
}
|
||||
|
||||
// Get returns a single agent capacity policy by ID.
|
||||
@@ -92,7 +95,7 @@ func (h *AgentCapacityHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, policy)
|
||||
c.JSON(http.StatusOK, serializeAgentCapacityPolicy(policy))
|
||||
}
|
||||
|
||||
// Update updates an existing agent capacity policy.
|
||||
@@ -110,8 +113,8 @@ func (h *AgentCapacityHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.UpdateAgentCapacityPolicyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req, err := bindAgentCapacityPolicyUpdate(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -119,11 +122,11 @@ func (h *AgentCapacityHandler) Update(c *gin.Context) {
|
||||
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)
|
||||
handleServiceError(c, svcErr)
|
||||
handleAgentCapacityError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, policy)
|
||||
c.JSON(http.StatusOK, serializeAgentCapacityPolicy(policy))
|
||||
}
|
||||
|
||||
// Delete deletes an agent capacity policy.
|
||||
@@ -147,7 +150,153 @@ func (h *AgentCapacityHandler) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(c)
|
||||
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 := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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 := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
limitID, err := parseUintParam(c, "limit_id")
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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 := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
limitID, err := parseUintParam(c, "limit_id")
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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 := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
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 := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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 := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
userID, err := parseUintParam(c, "user_id")
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// RegisterAgentCapacityRoutes registers agent capacity policy routes on a gin.RouterGroup.
|
||||
@@ -158,6 +307,125 @@ func RegisterAgentCapacityRoutes(rg *gin.RouterGroup, h *AgentCapacityHandler) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -31,7 +33,7 @@ func (s *AgentCapacityHandlerTestSuite) SetupSuite() {
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.User{}, &model.AgentCapacityPolicy{}))
|
||||
s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.AgentCapacityPolicy{}, &model.InboxCapacityLimit{}))
|
||||
s.db = db
|
||||
|
||||
repo := repository.NewAgentCapacityPolicyRepo(db)
|
||||
@@ -130,4 +132,79 @@ func (s *AgentCapacityHandlerTestSuite) TestDelete_BadRequest_InvalidID() {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AgentCapacityHandlerTestSuite) TestChatwootPolicyInboxLimitAndUserFlow() {
|
||||
r := gin.New()
|
||||
api := r.Group("/api/v1/accounts/:account_id")
|
||||
RegisterAgentCapacityRoutes(api, s.handler)
|
||||
|
||||
inbox := &model.Inbox{AccountID: s.account.ID, Name: "Priority", ChannelType: "web_widget"}
|
||||
s.Require().NoError(s.db.Create(inbox).Error)
|
||||
user := &model.User{AccountID: s.account.ID, Name: "Capacity Agent", Email: "capacity-agent@example.com", Password: "secret", Role: "agent"}
|
||||
s.Require().NoError(s.db.Create(user).Error)
|
||||
s.Require().NoError(s.db.Create(&model.AccountUser{AccountID: s.account.ID, UserID: user.ID, Role: "agent"}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"agent_capacity_policy":{"name":"Priority policy","description":"VIP","exclusion_rules":{"exclude_older_than_hours":24,"excluded_labels":["spam"]}}}`
|
||||
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/agent_capacity_policies/", s.account.ID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
s.Require().Equal(http.StatusOK, w.Code)
|
||||
var policy map[string]any
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &policy))
|
||||
s.Require().NotContains(policy, "success")
|
||||
s.Require().Equal("Priority policy", policy["name"])
|
||||
s.Require().Equal(float64(0), policy["assigned_agent_count"])
|
||||
policyID := uint(policy["id"].(float64))
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
body = fmt.Sprintf(`{"inbox_id":%d,"conversation_limit":7}`, inbox.ID)
|
||||
req, _ = http.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/agent_capacity_policies/%d/inbox_limits", s.account.ID, policyID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
s.Require().Equal(http.StatusOK, w.Code)
|
||||
var limit map[string]any
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &limit))
|
||||
s.Require().Equal(float64(inbox.ID), limit["inbox_id"])
|
||||
s.Require().Equal(float64(7), limit["conversation_limit"])
|
||||
limitID := uint(limit["id"].(float64))
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/agent_capacity_policies/%d/inbox_limits", s.account.ID, policyID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
s.Require().Equal(http.StatusUnprocessableEntity, w.Code)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/accounts/%d/agent_capacity_policies/%d/inbox_limits/%d", s.account.ID, policyID, limitID), bytes.NewBufferString(`{"conversation_limit":11}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
s.Require().Equal(http.StatusOK, w.Code)
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &limit))
|
||||
s.Require().Equal(float64(11), limit["conversation_limit"])
|
||||
s.Require().Equal("Priority", limit["inbox_name"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
body = fmt.Sprintf(`{"user_id":%d}`, user.ID)
|
||||
req, _ = http.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/%d/agent_capacity_policies/%d/users", s.account.ID, policyID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
s.Require().Equal(http.StatusOK, w.Code)
|
||||
var assigned map[string]any
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &assigned))
|
||||
s.Require().Equal(float64(user.ID), assigned["id"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/agent_capacity_policies/%d", s.account.ID, policyID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
s.Require().Equal(http.StatusOK, w.Code)
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &policy))
|
||||
s.Require().Equal(float64(1), policy["assigned_agent_count"])
|
||||
s.Require().Len(policy["inbox_capacity_limits"].([]any), 1)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/accounts/%d/agent_capacity_policies/%d/inbox_limits/%d", s.account.ID, policyID, limitID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
s.Require().Equal(http.StatusNoContent, w.Code)
|
||||
}
|
||||
|
||||
@@ -15,22 +15,24 @@ import (
|
||||
|
||||
// AccountUser represents a user's membership in an account with a specific role.
|
||||
type AccountUser struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;uniqueIndex:idx_user_account" json:"user_id"`
|
||||
AccountID uint `gorm:"not null;uniqueIndex:idx_user_account" json:"account_id"`
|
||||
Role string `gorm:"size:50;not null;default:'agent'" json:"role"` // agent, administrator, custom_role
|
||||
CustomRoleID uint `gorm:"default:0" json:"custom_role_id,omitempty"` // 0 means no custom role; >0 references CustomRole
|
||||
Availability string `gorm:"size:50;default:'offline'" json:"availability"` // online/offline/busy
|
||||
AutoOffline bool `gorm:"default:true" json:"auto_offline"` // auto-offline when idle
|
||||
ActiveAt *time.Time `json:"active_at,omitempty"` // last active timestamp in this account
|
||||
InvitedBy uint `gorm:"default:0" json:"invited_by"` // UserID of the user who invited this member
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;uniqueIndex:idx_user_account" json:"user_id"`
|
||||
AccountID uint `gorm:"not null;uniqueIndex:idx_user_account" json:"account_id"`
|
||||
Role string `gorm:"size:50;not null;default:'agent'" json:"role"` // agent, administrator, custom_role
|
||||
CustomRoleID uint `gorm:"default:0" json:"custom_role_id,omitempty"` // 0 means no custom role; >0 references CustomRole
|
||||
AgentCapacityPolicyID *uint `gorm:"index" json:"agent_capacity_policy_id,omitempty"` // enterprise capacity policy assignment
|
||||
Availability string `gorm:"size:50;default:'offline'" json:"availability"` // online/offline/busy
|
||||
AutoOffline bool `gorm:"default:true" json:"auto_offline"` // auto-offline when idle
|
||||
ActiveAt *time.Time `json:"active_at,omitempty"` // last active timestamp in this account
|
||||
InvitedBy uint `gorm:"default:0" json:"invited_by"` // UserID of the user who invited this member
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
|
||||
|
||||
// Relations
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"`
|
||||
AgentCapacityPolicy *AgentCapacityPolicy `gorm:"foreignKey:AgentCapacityPolicyID" json:"agent_capacity_policy,omitempty"`
|
||||
}
|
||||
|
||||
func (AccountUser) TableName() string { return "account_users" }
|
||||
@@ -53,4 +55,4 @@ func (au *AccountUser) HasCustomRole() bool {
|
||||
// IsOnline returns true if the agent availability is "online".
|
||||
func (au *AccountUser) IsOnline() bool {
|
||||
return au.Availability == "online"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,20 @@ type AgentCapacityPolicy struct {
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"`
|
||||
Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"`
|
||||
AccountUsers []AccountUser `gorm:"foreignKey:AgentCapacityPolicyID" json:"account_users,omitempty"`
|
||||
InboxCapacityLimits []InboxCapacityLimit `gorm:"foreignKey:AgentCapacityPolicyID" json:"inbox_capacity_limits,omitempty"`
|
||||
}
|
||||
|
||||
// InboxCapacityLimit associates a capacity policy with an inbox and its conversation limit.
|
||||
// Reference: Chatwoot InboxCapacityLimit + P2B M11 spec
|
||||
type InboxCapacityLimit struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AgentCapacityPolicyID uint `gorm:"not null;index" json:"agent_capacity_policy_id"`
|
||||
InboxID uint `gorm:"not null;index" json:"inbox_id"`
|
||||
ConversationLimit int `gorm:"not null;check:conversation_limit >= 0" json:"conversation_limit"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AgentCapacityPolicyID uint `gorm:"not null;index;uniqueIndex:idx_policy_inbox_capacity_limit" json:"agent_capacity_policy_id"`
|
||||
InboxID uint `gorm:"not null;index;uniqueIndex:idx_policy_inbox_capacity_limit" json:"inbox_id"`
|
||||
ConversationLimit int `gorm:"not null;check:conversation_limit >= 0" json:"conversation_limit"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
AgentCapacityPolicy AgentCapacityPolicy `gorm:"foreignKey:AgentCapacityPolicyID" json:"agent_capacity_policy,omitempty"`
|
||||
Inbox Inbox `gorm:"foreignKey:InboxID" json:"inbox,omitempty"`
|
||||
@@ -37,4 +38,4 @@ type InboxCapacityLimit struct {
|
||||
|
||||
func (InboxCapacityLimit) TableName() string { return "inbox_capacity_limits" }
|
||||
|
||||
func (AgentCapacityPolicy) TableName() string { return "agent_capacity_policies" }
|
||||
func (AgentCapacityPolicy) TableName() string { return "agent_capacity_policies" }
|
||||
|
||||
@@ -22,7 +22,10 @@ func NewAgentCapacityPolicyRepo(db *gorm.DB) *AgentCapacityPolicyRepo {
|
||||
// FindByID retrieves an agent capacity policy by primary key.
|
||||
func (r *AgentCapacityPolicyRepo) FindByID(ctx context.Context, id uint) (*model.AgentCapacityPolicy, error) {
|
||||
var policy model.AgentCapacityPolicy
|
||||
err := r.db.WithContext(ctx).First(&policy, id).Error
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("InboxCapacityLimits").
|
||||
Preload("AccountUsers").
|
||||
First(&policy, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,11 +42,87 @@ func (r *AgentCapacityPolicyRepo) FindByAccount(ctx context.Context, accountID u
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := q.Offset(offset).Limit(limit).
|
||||
err := q.Preload("InboxCapacityLimits").Preload("AccountUsers").
|
||||
Offset(offset).Limit(limit).
|
||||
Order("id ASC").Find(&policies).Error
|
||||
return policies, total, err
|
||||
}
|
||||
|
||||
// FindInboxByAccount retrieves an inbox scoped to an account.
|
||||
func (r *AgentCapacityPolicyRepo) FindInboxByAccount(ctx context.Context, accountID, inboxID uint) (*model.Inbox, error) {
|
||||
var inbox model.Inbox
|
||||
if err := r.db.WithContext(ctx).Where("account_id = ? AND id = ?", accountID, inboxID).First(&inbox).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &inbox, nil
|
||||
}
|
||||
|
||||
// CreateInboxCapacityLimit creates a per-inbox capacity limit under a policy.
|
||||
func (r *AgentCapacityPolicyRepo) CreateInboxCapacityLimit(ctx context.Context, limit *model.InboxCapacityLimit) error {
|
||||
return r.db.WithContext(ctx).Create(limit).Error
|
||||
}
|
||||
|
||||
// FindInboxCapacityLimitByPolicy retrieves an inbox capacity limit scoped to a policy.
|
||||
func (r *AgentCapacityPolicyRepo) FindInboxCapacityLimitByPolicy(ctx context.Context, policyID, limitID uint) (*model.InboxCapacityLimit, error) {
|
||||
var limit model.InboxCapacityLimit
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("Inbox").
|
||||
Where("agent_capacity_policy_id = ? AND id = ?", policyID, limitID).
|
||||
First(&limit).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &limit, nil
|
||||
}
|
||||
|
||||
// FindInboxCapacityLimitByInbox retrieves an inbox capacity limit scoped by policy and inbox.
|
||||
func (r *AgentCapacityPolicyRepo) FindInboxCapacityLimitByInbox(ctx context.Context, policyID, inboxID uint) (*model.InboxCapacityLimit, error) {
|
||||
var limit model.InboxCapacityLimit
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("agent_capacity_policy_id = ? AND inbox_id = ?", policyID, inboxID).
|
||||
First(&limit).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &limit, nil
|
||||
}
|
||||
|
||||
// UpdateInboxCapacityLimit updates an inbox capacity limit.
|
||||
func (r *AgentCapacityPolicyRepo) UpdateInboxCapacityLimit(ctx context.Context, limit *model.InboxCapacityLimit) error {
|
||||
return r.db.WithContext(ctx).Save(limit).Error
|
||||
}
|
||||
|
||||
// DeleteInboxCapacityLimit deletes an inbox capacity limit.
|
||||
func (r *AgentCapacityPolicyRepo) DeleteInboxCapacityLimit(ctx context.Context, limit *model.InboxCapacityLimit) error {
|
||||
return r.db.WithContext(ctx).Delete(limit).Error
|
||||
}
|
||||
|
||||
// FindPolicyUsers retrieves users assigned to a capacity policy via account_users.
|
||||
func (r *AgentCapacityPolicyRepo) FindPolicyUsers(ctx context.Context, accountID, policyID uint) ([]model.User, error) {
|
||||
var users []model.User
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN account_users ON account_users.user_id = users.id").
|
||||
Where("account_users.account_id = ? AND account_users.agent_capacity_policy_id = ?", accountID, policyID).
|
||||
Order("users.id ASC").
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
// FindAccountUser retrieves a user's membership in an account.
|
||||
func (r *AgentCapacityPolicyRepo) FindAccountUser(ctx context.Context, accountID, userID uint) (*model.AccountUser, error) {
|
||||
var accountUser model.AccountUser
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("User").
|
||||
Where("account_id = ? AND user_id = ?", accountID, userID).
|
||||
First(&accountUser).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &accountUser, nil
|
||||
}
|
||||
|
||||
// UpdateAccountUser updates an account membership.
|
||||
func (r *AgentCapacityPolicyRepo) UpdateAccountUser(ctx context.Context, accountUser *model.AccountUser) error {
|
||||
return r.db.WithContext(ctx).Save(accountUser).Error
|
||||
}
|
||||
|
||||
// Create creates a new agent capacity policy.
|
||||
func (r *AgentCapacityPolicyRepo) Create(ctx context.Context, policy *model.AgentCapacityPolicy) error {
|
||||
return r.db.WithContext(ctx).Create(policy).Error
|
||||
@@ -57,4 +136,4 @@ func (r *AgentCapacityPolicyRepo) Update(ctx context.Context, policy *model.Agen
|
||||
// Delete deletes an agent capacity policy by ID.
|
||||
func (r *AgentCapacityPolicyRepo) Delete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.AgentCapacityPolicy{}, id).Error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AgentCapacityPolicyService implements business logic for AgentCapacityPolicy operations.
|
||||
@@ -26,19 +28,32 @@ func NewAgentCapacityPolicyService(repo *repository.AgentCapacityPolicyRepo) *Ag
|
||||
type CreateAgentCapacityPolicyRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AssignmentLogic string `json:"assignment_logic" validate:"required"`
|
||||
AssignmentLogic string `json:"assignment_logic,omitempty"`
|
||||
ExclusionRules json.RawMessage `json:"exclusion_rules,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateAgentCapacityPolicyRequest is the DTO for updating an agent capacity policy.
|
||||
// Request body uses Chatwoot-style wrapper: { "agent_capacity_policy": { ... } }
|
||||
type UpdateAgentCapacityPolicyRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
AssignmentLogic string `json:"assignment_logic,omitempty"`
|
||||
ExclusionRules json.RawMessage `json:"exclusion_rules,omitempty"`
|
||||
}
|
||||
|
||||
type CreateInboxCapacityLimitRequest struct {
|
||||
InboxID uint `json:"inbox_id"`
|
||||
ConversationLimit int `json:"conversation_limit"`
|
||||
}
|
||||
|
||||
type UpdateInboxCapacityLimitRequest struct {
|
||||
ConversationLimit int `json:"conversation_limit"`
|
||||
}
|
||||
|
||||
type AssignCapacityPolicyUserRequest struct {
|
||||
UserID uint `json:"user_id"`
|
||||
}
|
||||
|
||||
// validAssignmentLogic values allowed for AssignmentLogic field.
|
||||
var validAssignmentLogic = map[string]bool{
|
||||
"round_robin": true,
|
||||
@@ -47,6 +62,9 @@ var validAssignmentLogic = map[string]bool{
|
||||
|
||||
// validateAssignmentLogic checks that the assignment logic is one of the allowed values.
|
||||
func validateAssignmentLogic(logic string) error {
|
||||
if logic == "" {
|
||||
return nil
|
||||
}
|
||||
if !validAssignmentLogic[logic] {
|
||||
return fmt.Errorf("invalid assignment_logic: must be one of round_robin, least_busy")
|
||||
}
|
||||
@@ -81,16 +99,30 @@ func (s *AgentCapacityPolicyService) List(ctx context.Context, accountID uint, p
|
||||
|
||||
// Create creates a new agent capacity policy within an account.
|
||||
func (s *AgentCapacityPolicyService) Create(ctx context.Context, accountID uint, req CreateAgentCapacityPolicyRequest) (*model.AgentCapacityPolicy, error) {
|
||||
if req.Name == "" {
|
||||
return nil, fmt.Errorf("name is required")
|
||||
}
|
||||
if len(req.Name) > 255 {
|
||||
return nil, fmt.Errorf("name is too long")
|
||||
}
|
||||
if err := validateAssignmentLogic(req.AssignmentLogic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assignmentLogic := req.AssignmentLogic
|
||||
if assignmentLogic == "" {
|
||||
assignmentLogic = "round_robin"
|
||||
}
|
||||
exclusionRules := req.ExclusionRules
|
||||
if len(exclusionRules) == 0 {
|
||||
exclusionRules = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
policy := &model.AgentCapacityPolicy{
|
||||
AccountID: accountID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
AssignmentLogic: req.AssignmentLogic,
|
||||
ExclusionRules: req.ExclusionRules,
|
||||
AssignmentLogic: assignmentLogic,
|
||||
ExclusionRules: exclusionRules,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(ctx, policy); err != nil {
|
||||
@@ -111,11 +143,17 @@ func (s *AgentCapacityPolicyService) Update(ctx context.Context, id, accountID u
|
||||
}
|
||||
|
||||
// Merge fields: only update non-empty values
|
||||
if req.Name != "" {
|
||||
policy.Name = req.Name
|
||||
if req.Name != nil {
|
||||
if *req.Name == "" {
|
||||
return nil, fmt.Errorf("name is required")
|
||||
}
|
||||
if len(*req.Name) > 255 {
|
||||
return nil, fmt.Errorf("name is too long")
|
||||
}
|
||||
policy.Name = *req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
policy.Description = req.Description
|
||||
if req.Description != nil {
|
||||
policy.Description = *req.Description
|
||||
}
|
||||
if req.AssignmentLogic != "" {
|
||||
if err := validateAssignmentLogic(req.AssignmentLogic); err != nil {
|
||||
@@ -134,6 +172,113 @@ func (s *AgentCapacityPolicyService) Update(ctx context.Context, id, accountID u
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (s *AgentCapacityPolicyService) CreateInboxCapacityLimit(ctx context.Context, policyID, accountID uint, req CreateInboxCapacityLimitRequest) (*model.InboxCapacityLimit, error) {
|
||||
policy, err := s.GetByID(ctx, policyID, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.InboxID == 0 {
|
||||
return nil, fmt.Errorf("inbox_id is required")
|
||||
}
|
||||
if req.ConversationLimit < 0 {
|
||||
return nil, fmt.Errorf("conversation_limit must be greater than or equal to 0")
|
||||
}
|
||||
if _, err := s.repo.FindInboxByAccount(ctx, accountID, req.InboxID); err != nil {
|
||||
return nil, fmt.Errorf("inbox not found: %w", err)
|
||||
}
|
||||
if _, err := s.repo.FindInboxCapacityLimitByInbox(ctx, policy.ID, req.InboxID); err == nil {
|
||||
return nil, fmt.Errorf("inbox has already been assigned to this policy")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("failed to check inbox capacity limit: %w", err)
|
||||
}
|
||||
|
||||
limit := &model.InboxCapacityLimit{
|
||||
AgentCapacityPolicyID: policy.ID,
|
||||
InboxID: req.InboxID,
|
||||
ConversationLimit: req.ConversationLimit,
|
||||
}
|
||||
if err := s.repo.CreateInboxCapacityLimit(ctx, limit); err != nil {
|
||||
return nil, fmt.Errorf("failed to create inbox capacity limit: %w", err)
|
||||
}
|
||||
return s.repo.FindInboxCapacityLimitByPolicy(ctx, policy.ID, limit.ID)
|
||||
}
|
||||
|
||||
func (s *AgentCapacityPolicyService) UpdateInboxCapacityLimit(ctx context.Context, policyID, accountID, limitID uint, req UpdateInboxCapacityLimitRequest) (*model.InboxCapacityLimit, error) {
|
||||
if _, err := s.GetByID(ctx, policyID, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.ConversationLimit < 0 {
|
||||
return nil, fmt.Errorf("conversation_limit must be greater than or equal to 0")
|
||||
}
|
||||
limit, err := s.repo.FindInboxCapacityLimitByPolicy(ctx, policyID, limitID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inbox capacity limit not found: %w", err)
|
||||
}
|
||||
limit.ConversationLimit = req.ConversationLimit
|
||||
if err := s.repo.UpdateInboxCapacityLimit(ctx, limit); err != nil {
|
||||
return nil, fmt.Errorf("failed to update inbox capacity limit: %w", err)
|
||||
}
|
||||
return s.repo.FindInboxCapacityLimitByPolicy(ctx, policyID, limitID)
|
||||
}
|
||||
|
||||
func (s *AgentCapacityPolicyService) DeleteInboxCapacityLimit(ctx context.Context, policyID, accountID, limitID uint) error {
|
||||
if _, err := s.GetByID(ctx, policyID, accountID); err != nil {
|
||||
return err
|
||||
}
|
||||
limit, err := s.repo.FindInboxCapacityLimitByPolicy(ctx, policyID, limitID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inbox capacity limit not found: %w", err)
|
||||
}
|
||||
if err := s.repo.DeleteInboxCapacityLimit(ctx, limit); err != nil {
|
||||
return fmt.Errorf("failed to delete inbox capacity limit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AgentCapacityPolicyService) ListUsers(ctx context.Context, policyID, accountID uint) ([]model.User, error) {
|
||||
if _, err := s.GetByID(ctx, policyID, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := s.repo.FindPolicyUsers(ctx, accountID, policyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list agent capacity policy users: %w", err)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (s *AgentCapacityPolicyService) AssignUser(ctx context.Context, policyID, accountID uint, req AssignCapacityPolicyUserRequest) (*model.User, error) {
|
||||
if _, err := s.GetByID(ctx, policyID, accountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.UserID == 0 {
|
||||
return nil, fmt.Errorf("user_id is required")
|
||||
}
|
||||
accountUser, err := s.repo.FindAccountUser(ctx, accountID, req.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account user not found: %w", err)
|
||||
}
|
||||
accountUser.AgentCapacityPolicyID = &policyID
|
||||
if err := s.repo.UpdateAccountUser(ctx, accountUser); err != nil {
|
||||
return nil, fmt.Errorf("failed to assign user to capacity policy: %w", err)
|
||||
}
|
||||
return &accountUser.User, nil
|
||||
}
|
||||
|
||||
func (s *AgentCapacityPolicyService) RemoveUser(ctx context.Context, policyID, accountID, userID uint) error {
|
||||
if _, err := s.GetByID(ctx, policyID, accountID); err != nil {
|
||||
return err
|
||||
}
|
||||
accountUser, err := s.repo.FindAccountUser(ctx, accountID, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("account user not found: %w", err)
|
||||
}
|
||||
accountUser.AgentCapacityPolicyID = nil
|
||||
if err := s.repo.UpdateAccountUser(ctx, accountUser); err != nil {
|
||||
return fmt.Errorf("failed to remove user from capacity policy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes an agent capacity policy scoped to an account.
|
||||
func (s *AgentCapacityPolicyService) Delete(ctx context.Context, id, accountID uint) error {
|
||||
policy, err := s.repo.FindByID(ctx, id)
|
||||
@@ -161,4 +306,4 @@ func (s *AgentCapacityPolicyService) GetByID(ctx context.Context, id, accountID
|
||||
return nil, fmt.Errorf("agent capacity policy not found: policy does not belong to account %d", accountID)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ func setupACPolicyServiceTest(t *testing.T) (*AgentCapacityPolicyService, *gorm.
|
||||
|
||||
require.NoError(t, db.AutoMigrate(
|
||||
&model.Account{},
|
||||
&model.User{},
|
||||
&model.AccountUser{},
|
||||
&model.Inbox{},
|
||||
&model.AgentCapacityPolicy{},
|
||||
&model.InboxCapacityLimit{},
|
||||
), "failed to auto-migrate")
|
||||
|
||||
t.Cleanup(func() {
|
||||
@@ -40,6 +44,8 @@ func setupACPolicyServiceTest(t *testing.T) (*AgentCapacityPolicyService, *gorm.
|
||||
return svc, db
|
||||
}
|
||||
|
||||
func acStringPtr(value string) *string { return &value }
|
||||
|
||||
func createACPSvcTestAccount(t *testing.T, db *gorm.DB) *model.Account {
|
||||
t.Helper()
|
||||
account := &model.Account{Name: "ACPServiceOrg", Locale: "en", Active: true}
|
||||
@@ -90,9 +96,8 @@ func TestAgentCapacityPolicyService_Create(t *testing.T) {
|
||||
account := createACPSvcTestAccount(t, db)
|
||||
|
||||
policy, err := svc.Create(context.Background(), account.ID, CreateAgentCapacityPolicyRequest{
|
||||
Name: "RoundRobinPolicy",
|
||||
AssignmentLogic: "round_robin",
|
||||
ExclusionRules: json.RawMessage(`{"exclude_offline":true}`),
|
||||
Name: "RoundRobinPolicy",
|
||||
ExclusionRules: json.RawMessage(`{"exclude_offline":true}`),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, policy.ID)
|
||||
@@ -164,7 +169,7 @@ func TestAgentCapacityPolicyService_Update(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := svc.Update(context.Background(), created.ID, account.ID, UpdateAgentCapacityPolicyRequest{
|
||||
Name: "NewPolicy",
|
||||
Name: acStringPtr("NewPolicy"),
|
||||
AssignmentLogic: "least_busy",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
@@ -192,11 +197,76 @@ func TestAgentCapacityPolicyService_Update_NotFound(t *testing.T) {
|
||||
svc, _ := setupACPolicyServiceTest(t)
|
||||
|
||||
_, err := svc.Update(context.Background(), 9999, 1, UpdateAgentCapacityPolicyRequest{
|
||||
Name: "Ghost",
|
||||
Name: acStringPtr("Ghost"),
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAgentCapacityPolicyService_InboxCapacityLimitFlow(t *testing.T) {
|
||||
svc, db := setupACPolicyServiceTest(t)
|
||||
account := createACPSvcTestAccount(t, db)
|
||||
inbox := &model.Inbox{AccountID: account.ID, Name: "Priority", ChannelType: "web_widget"}
|
||||
require.NoError(t, db.Create(inbox).Error)
|
||||
otherAccount := createACPSvcTestAccount(t, db)
|
||||
otherInbox := &model.Inbox{AccountID: otherAccount.ID, Name: "Other", ChannelType: "web_widget"}
|
||||
require.NoError(t, db.Create(otherInbox).Error)
|
||||
policy, err := svc.Create(context.Background(), account.ID, CreateAgentCapacityPolicyRequest{Name: "Policy"})
|
||||
require.NoError(t, err)
|
||||
|
||||
limit, err := svc.CreateInboxCapacityLimit(context.Background(), policy.ID, account.ID, CreateInboxCapacityLimitRequest{
|
||||
InboxID: inbox.ID,
|
||||
ConversationLimit: 7,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, inbox.ID, limit.InboxID)
|
||||
assert.Equal(t, 7, limit.ConversationLimit)
|
||||
|
||||
_, err = svc.CreateInboxCapacityLimit(context.Background(), policy.ID, account.ID, CreateInboxCapacityLimitRequest{InboxID: inbox.ID, ConversationLimit: 9})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already been assigned")
|
||||
|
||||
_, err = svc.CreateInboxCapacityLimit(context.Background(), policy.ID, account.ID, CreateInboxCapacityLimitRequest{InboxID: otherInbox.ID, ConversationLimit: 1})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "inbox not found")
|
||||
|
||||
_, err = svc.UpdateInboxCapacityLimit(context.Background(), policy.ID, account.ID, limit.ID, UpdateInboxCapacityLimitRequest{ConversationLimit: -1})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "greater than or equal")
|
||||
|
||||
updated, err := svc.UpdateInboxCapacityLimit(context.Background(), policy.ID, account.ID, limit.ID, UpdateInboxCapacityLimitRequest{ConversationLimit: 11})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 11, updated.ConversationLimit)
|
||||
assert.Equal(t, "Priority", updated.Inbox.Name)
|
||||
|
||||
require.NoError(t, svc.DeleteInboxCapacityLimit(context.Background(), policy.ID, account.ID, limit.ID))
|
||||
_, err = svc.UpdateInboxCapacityLimit(context.Background(), policy.ID, account.ID, limit.ID, UpdateInboxCapacityLimitRequest{ConversationLimit: 1})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAgentCapacityPolicyService_UserAssignmentFlow(t *testing.T) {
|
||||
svc, db := setupACPolicyServiceTest(t)
|
||||
account := createACPSvcTestAccount(t, db)
|
||||
policy, err := svc.Create(context.Background(), account.ID, CreateAgentCapacityPolicyRequest{Name: "Policy"})
|
||||
require.NoError(t, err)
|
||||
user := &model.User{AccountID: account.ID, Name: "Agent", Email: "agent@example.com", Password: "secret", Role: "agent"}
|
||||
require.NoError(t, db.Create(user).Error)
|
||||
accountUser := &model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent"}
|
||||
require.NoError(t, db.Create(accountUser).Error)
|
||||
|
||||
assigned, err := svc.AssignUser(context.Background(), policy.ID, account.ID, AssignCapacityPolicyUserRequest{UserID: user.ID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user.ID, assigned.ID)
|
||||
|
||||
users, err := svc.ListUsers(context.Background(), policy.ID, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, users, 1)
|
||||
|
||||
require.NoError(t, svc.RemoveUser(context.Background(), policy.ID, account.ID, user.ID))
|
||||
users, err = svc.ListUsers(context.Background(), policy.ID, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, users)
|
||||
}
|
||||
|
||||
// ========== Delete ==========
|
||||
|
||||
func TestAgentCapacityPolicyService_Delete(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
DROP INDEX IF EXISTS idx_inbox_capacity_limits_inbox_id;
|
||||
DROP INDEX IF EXISTS idx_inbox_capacity_limits_policy_id;
|
||||
DROP TABLE IF EXISTS inbox_capacity_limits;
|
||||
|
||||
DROP INDEX IF EXISTS idx_agent_capacity_policies_account_id;
|
||||
DROP TABLE IF EXISTS agent_capacity_policies;
|
||||
|
||||
DROP INDEX IF EXISTS idx_account_users_agent_capacity_policy_id;
|
||||
ALTER TABLE account_users DROP COLUMN IF EXISTS agent_capacity_policy_id;
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Chatwoot enterprise agent capacity policy links.
|
||||
|
||||
ALTER TABLE account_users ADD COLUMN IF NOT EXISTS agent_capacity_policy_id INTEGER;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_account_users_agent_capacity_policy_id
|
||||
ON account_users(agent_capacity_policy_id)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_capacity_policies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
assignment_logic VARCHAR(50) DEFAULT 'round_robin',
|
||||
exclusion_rules JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_capacity_policies_account_id
|
||||
ON agent_capacity_policies(account_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inbox_capacity_limits (
|
||||
id SERIAL PRIMARY KEY,
|
||||
agent_capacity_policy_id INTEGER NOT NULL,
|
||||
inbox_id INTEGER NOT NULL,
|
||||
conversation_limit INTEGER NOT NULL CHECK (conversation_limit >= 0),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(agent_capacity_policy_id, inbox_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inbox_capacity_limits_policy_id
|
||||
ON inbox_capacity_limits(agent_capacity_policy_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inbox_capacity_limits_inbox_id
|
||||
ON inbox_capacity_limits(inbox_id);
|
||||
Reference in New Issue
Block a user