feat(assignment-policies): align chatwoot payloads
This commit is contained in:
+4
-1
@@ -209,7 +209,10 @@ func autoMigrate(db *gorm.DB) error {
|
||||
&model.SlaPolicyInbox{},
|
||||
&model.AppliedSLA{},
|
||||
&model.SlaEvent{},
|
||||
// M11: Assignment Policy V2 model — enhanced with Type field + inbox join
|
||||
// Chatwoot assignment policies + inbox association.
|
||||
&model.AssignmentPolicy{},
|
||||
&model.InboxAssignmentPolicy{},
|
||||
// Legacy enhanced assignment policy aliases kept for local compatibility.
|
||||
&model.AssignmentPolicyV2{},
|
||||
&model.AssignmentPolicyInbox{},
|
||||
// Enterprise: Audit log model (read-only log of account actions)
|
||||
|
||||
@@ -17,6 +17,25 @@ type AssignmentPolicyHandler struct {
|
||||
svc *service.AssignmentPolicyService
|
||||
}
|
||||
|
||||
// ListAccountPolicies returns all assignment policies for the account.
|
||||
// GET /api/v1/accounts/:id/assignment_policies
|
||||
func (h *AssignmentPolicyHandler) ListAccountPolicies(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
|
||||
return
|
||||
}
|
||||
|
||||
policies, svcErr := h.svc.ListAccountPolicies(c.Request.Context(), accountID)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("List assignment policies for account %d: %v", accountID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, policies)
|
||||
}
|
||||
|
||||
// NewAssignmentPolicyHandler creates a new AssignmentPolicy handler.
|
||||
func NewAssignmentPolicyHandler(svc *service.AssignmentPolicyService) *AssignmentPolicyHandler {
|
||||
return &AssignmentPolicyHandler{svc: svc}
|
||||
@@ -31,14 +50,29 @@ func (h *AssignmentPolicyHandler) GetAccountPolicy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
policy, svcErr := h.svc.GetAccountPolicy(c.Request.Context(), accountID)
|
||||
var policyID uint
|
||||
if raw := c.Param("policy_id"); raw != "" {
|
||||
id, err := strconv.ParseUint(raw, 10, 32)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid policy ID")
|
||||
return
|
||||
}
|
||||
policyID = uint(id)
|
||||
}
|
||||
|
||||
policy, svcErr := h.svc.GetAccountPolicy(c.Request.Context(), accountID, policyID)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("Get assignment policy for account %d: %v", accountID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, policy)
|
||||
payload, svcErr := h.svc.SerializePolicy(c.Request.Context(), policy)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// CreateAccountPolicy creates the account-level assignment policy.
|
||||
@@ -50,20 +84,25 @@ func (h *AssignmentPolicyHandler) CreateAccountPolicy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.CreatePolicyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req, err := bindAssignmentPolicyCreate(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
policy, svcErr := h.svc.CreateAccountPolicy(c.Request.Context(), accountID, req)
|
||||
policy, svcErr := h.svc.CreateAccountPolicy(c.Request.Context(), accountID, *req)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("Create assignment policy for account %d: %v", accountID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, policy)
|
||||
payload, svcErr := h.svc.SerializePolicy(c.Request.Context(), policy)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// UpdateAccountPolicy updates the account-level assignment policy.
|
||||
@@ -81,20 +120,25 @@ func (h *AssignmentPolicyHandler) UpdateAccountPolicy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.UpdatePolicyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req, err := bindAssignmentPolicyUpdate(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
policy, svcErr := h.svc.UpdateAccountPolicy(c.Request.Context(), uint(id), accountID, req)
|
||||
policy, svcErr := h.svc.UpdateAccountPolicy(c.Request.Context(), uint(id), accountID, *req)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("Update assignment policy %d for account %d: %v", id, accountID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, policy)
|
||||
payload, svcErr := h.svc.SerializePolicy(c.Request.Context(), policy)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// DeleteAccountPolicy deletes the account-level assignment policy.
|
||||
@@ -118,7 +162,7 @@ func (h *AssignmentPolicyHandler) DeleteAccountPolicy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(c)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// GetInboxPolicy returns the inbox-level assignment policy override.
|
||||
@@ -143,7 +187,86 @@ func (h *AssignmentPolicyHandler) GetInboxPolicy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, policy)
|
||||
payload, svcErr := h.svc.SerializePolicy(c.Request.Context(), policy)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// ListPolicyInboxes returns Chatwoot `{ inboxes: [...] }` payload for a policy.
|
||||
// GET /api/v1/accounts/:id/assignment_policies/:policy_id/inboxes
|
||||
func (h *AssignmentPolicyHandler) ListPolicyInboxes(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
|
||||
return
|
||||
}
|
||||
policyID, err := strconv.ParseUint(c.Param("policy_id"), 10, 32)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid policy ID")
|
||||
return
|
||||
}
|
||||
|
||||
inboxes, svcErr := h.svc.ListPolicyInboxes(c.Request.Context(), accountID, uint(policyID))
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"inboxes": inboxes})
|
||||
}
|
||||
|
||||
// AddPolicyInbox associates an inbox with a policy through the nested policy route.
|
||||
// POST /api/v1/accounts/:id/assignment_policies/:policy_id/inboxes
|
||||
func (h *AssignmentPolicyHandler) AddPolicyInbox(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
|
||||
return
|
||||
}
|
||||
policyID, err := strconv.ParseUint(c.Param("policy_id"), 10, 32)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid policy ID")
|
||||
return
|
||||
}
|
||||
var req service.CreateInboxPolicyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
req.AssignmentPolicyID = uint(policyID)
|
||||
policy, svcErr := h.svc.CreateInboxPolicy(c.Request.Context(), accountID, req)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
payload, svcErr := h.svc.SerializePolicy(c.Request.Context(), policy)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// RemovePolicyInbox removes the current policy association from an inbox.
|
||||
// DELETE /api/v1/accounts/:id/assignment_policies/:policy_id/inboxes/:inbox_id
|
||||
func (h *AssignmentPolicyHandler) RemovePolicyInbox(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
|
||||
return
|
||||
}
|
||||
inboxID, err := strconv.ParseUint(c.Param("inbox_id"), 10, 32)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid inbox ID")
|
||||
return
|
||||
}
|
||||
if svcErr := h.svc.DeleteInboxPolicy(c.Request.Context(), uint(inboxID), accountID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// CreateInboxPolicy creates an inbox-level assignment policy override.
|
||||
@@ -163,7 +286,7 @@ func (h *AssignmentPolicyHandler) CreateInboxPolicy(c *gin.Context) {
|
||||
|
||||
var req service.CreateInboxPolicyRequest
|
||||
req.InboxID = uint(inboxID) // Set from URL param
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := bindAssignmentPolicyBody(c, &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -175,7 +298,12 @@ func (h *AssignmentPolicyHandler) CreateInboxPolicy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, policy)
|
||||
payload, svcErr := h.svc.SerializePolicy(c.Request.Context(), policy)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// UpdateInboxPolicy updates an inbox-level assignment policy override.
|
||||
@@ -230,5 +358,52 @@ func (h *AssignmentPolicyHandler) DeleteInboxPolicy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(c)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteCurrentInboxPolicy removes the policy association for the URL inbox.
|
||||
// DELETE /api/v1/accounts/:id/inboxes/:inbox_id/assignment_policy
|
||||
func (h *AssignmentPolicyHandler) DeleteCurrentInboxPolicy(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified")
|
||||
return
|
||||
}
|
||||
inboxID, err := strconv.ParseUint(c.Param("inbox_id"), 10, 32)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid inbox ID")
|
||||
return
|
||||
}
|
||||
if svcErr := h.svc.DeleteInboxPolicy(c.Request.Context(), uint(inboxID), accountID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func bindAssignmentPolicyCreate(c *gin.Context) (*service.CreatePolicyRequest, error) {
|
||||
var req service.CreatePolicyRequest
|
||||
if err := bindChatwootPayload(c, "assignment_policy", &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func bindAssignmentPolicyUpdate(c *gin.Context) (*service.UpdatePolicyRequest, error) {
|
||||
var req service.UpdatePolicyRequest
|
||||
if err := bindChatwootPayload(c, "assignment_policy", &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func bindAssignmentPolicyBody(c *gin.Context, req *service.CreateInboxPolicyRequest) error {
|
||||
var body struct {
|
||||
AssignmentPolicyID uint `json:"assignment_policy_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
req.AssignmentPolicyID = body.AssignmentPolicyID
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,8 +40,9 @@ func (s *AssignmentPolicyHandlerTestSuite) SetupSuite() {
|
||||
// AutoMigrate all required models
|
||||
err = db.AutoMigrate(
|
||||
&model.Account{},
|
||||
&autoassignment.AssignmentPolicy{},
|
||||
&autoassignment.InboxAssignmentPolicy{},
|
||||
&model.Inbox{},
|
||||
&model.AssignmentPolicy{},
|
||||
&model.InboxAssignmentPolicy{},
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -76,6 +77,14 @@ func (s *AssignmentPolicyHandlerTestSuite) SetupSuite() {
|
||||
rg.POST("/assignment_policy", s.handler.CreateAccountPolicy)
|
||||
rg.PUT("/assignment_policy/:policy_id", s.handler.UpdateAccountPolicy)
|
||||
rg.DELETE("/assignment_policy/:policy_id", s.handler.DeleteAccountPolicy)
|
||||
rg.GET("/assignment_policies", s.handler.ListAccountPolicies)
|
||||
rg.POST("/assignment_policies", s.handler.CreateAccountPolicy)
|
||||
rg.GET("/assignment_policies/:policy_id", s.handler.GetAccountPolicy)
|
||||
rg.PATCH("/assignment_policies/:policy_id", s.handler.UpdateAccountPolicy)
|
||||
rg.GET("/assignment_policies/:policy_id/inboxes", s.handler.ListPolicyInboxes)
|
||||
rg.POST("/inboxes/:inbox_id/assignment_policy", s.handler.CreateInboxPolicy)
|
||||
rg.GET("/inboxes/:inbox_id/assignment_policy", s.handler.GetInboxPolicy)
|
||||
rg.DELETE("/inboxes/:inbox_id/assignment_policy", s.handler.DeleteCurrentInboxPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +115,14 @@ func TestAssignmentPolicyHandlerTestSuite(t *testing.T) {
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestGetAccountPolicy_Success() {
|
||||
// Create a policy via the service (since the POST endpoint also tests creation, use DB directly for setup)
|
||||
policy := &autoassignment.AssignmentPolicy{
|
||||
policy := &model.AssignmentPolicy{
|
||||
AccountID: 1,
|
||||
Policy: autoassignment.PolicyRoundRobin,
|
||||
Name: "Default",
|
||||
AssignmentOrder: 0,
|
||||
ConversationPriority: 1,
|
||||
FairDistributionLimit: 5,
|
||||
FairDistributionWindow: 300,
|
||||
Active: true,
|
||||
Enabled: true,
|
||||
}
|
||||
s.Require().NoError(s.db.Create(policy).Error)
|
||||
|
||||
@@ -123,11 +134,9 @@ func (s *AssignmentPolicyHandlerTestSuite) TestGetAccountPolicy_Success() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.Equal(s.T(), float64(1), data["account_id"])
|
||||
assert.Equal(s.T(), "round_robin", data["policy"])
|
||||
assert.Equal(s.T(), "Default", resp["name"])
|
||||
assert.Equal(s.T(), float64(1), resp["conversation_priority"])
|
||||
assert.Equal(s.T(), true, resp["enabled"])
|
||||
}
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestGetAccountPolicy_NotFound() {
|
||||
@@ -163,23 +172,19 @@ func (s *AssignmentPolicyHandlerTestSuite) TestGetAccountPolicy_Unauthorized() {
|
||||
// ==================== CreateAccountPolicy ====================
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_Success() {
|
||||
body := `{"policy":"round_robin","fair_distribution_limit":5,"fair_distribution_window":300,"active":true}`
|
||||
body := `{"name":"Default","assignment_order":0,"conversation_priority":1,"fair_distribution_limit":5,"fair_distribution_window":300,"enabled":true}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/assignment_policy", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.Equal(s.T(), float64(1), data["account_id"])
|
||||
assert.Equal(s.T(), "round_robin", data["policy"])
|
||||
assert.Equal(s.T(), float64(5), data["fair_distribution_limit"])
|
||||
assert.Equal(s.T(), float64(300), data["fair_distribution_window"])
|
||||
assert.Equal(s.T(), "Default", resp["name"])
|
||||
assert.Equal(s.T(), float64(5), resp["fair_distribution_limit"])
|
||||
assert.Equal(s.T(), float64(300), resp["fair_distribution_window"])
|
||||
}
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_InvalidJSON() {
|
||||
@@ -218,7 +223,7 @@ func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_Unauthorized(
|
||||
unauthRouter := gin.New()
|
||||
unauthRouter.POST("/api/v1/accounts/:account_id/assignment_policy", s.handler.CreateAccountPolicy)
|
||||
|
||||
body := `{"policy":"round_robin"}`
|
||||
body := `{"name":"Default"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/assignment_policy", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -231,16 +236,16 @@ func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_Unauthorized(
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_Success() {
|
||||
// Create a policy first via DB
|
||||
policy := &autoassignment.AssignmentPolicy{
|
||||
policy := &model.AssignmentPolicy{
|
||||
AccountID: 1,
|
||||
Policy: autoassignment.PolicyRoundRobin,
|
||||
Name: "Default",
|
||||
FairDistributionLimit: 5,
|
||||
FairDistributionWindow: 300,
|
||||
Active: true,
|
||||
Enabled: true,
|
||||
}
|
||||
s.Require().NoError(s.db.Create(policy).Error)
|
||||
|
||||
body := `{"policy":"longest_waiting","fair_distribution_limit":10}`
|
||||
body := `{"name":"Priority","fair_distribution_limit":10}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/assignment_policy/"+uintToStr(policy.ID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -250,15 +255,12 @@ func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_Success() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.Equal(s.T(), "longest_waiting", data["policy"])
|
||||
assert.Equal(s.T(), float64(10), data["fair_distribution_limit"])
|
||||
assert.Equal(s.T(), "Priority", resp["name"])
|
||||
assert.Equal(s.T(), float64(10), resp["fair_distribution_limit"])
|
||||
}
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_NotFound() {
|
||||
body := `{"policy":"round_robin"}`
|
||||
body := `{"name":"Default"}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/assignment_policy/9999", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -281,12 +283,12 @@ func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_InvalidJSON()
|
||||
// ==================== DeleteAccountPolicy ====================
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestDeleteAccountPolicy_Success() {
|
||||
policy := &autoassignment.AssignmentPolicy{
|
||||
policy := &model.AssignmentPolicy{
|
||||
AccountID: 1,
|
||||
Policy: autoassignment.PolicyRoundRobin,
|
||||
Name: "Default",
|
||||
FairDistributionLimit: 5,
|
||||
FairDistributionWindow: 300,
|
||||
Active: true,
|
||||
Enabled: true,
|
||||
}
|
||||
s.Require().NoError(s.db.Create(policy).Error)
|
||||
|
||||
@@ -294,7 +296,58 @@ func (s *AssignmentPolicyHandlerTestSuite) TestDeleteAccountPolicy_Success() {
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/assignment_policy/"+uintToStr(policy.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusNoContent, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestPluralAssignmentPolicies_ChatwootPayloads() {
|
||||
policy := &model.AssignmentPolicy{AccountID: 1, Name: "Balanced", Description: "Route VIP first", AssignmentOrder: 1, ConversationPriority: 1, FairDistributionLimit: 20, FairDistributionWindow: 1800, Enabled: true}
|
||||
s.Require().NoError(s.db.Create(policy).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/assignment_policies", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var list []map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &list))
|
||||
s.Require().Len(list, 1)
|
||||
assert.Equal(s.T(), "Balanced", list[0]["name"])
|
||||
assert.Contains(s.T(), list[0], "assigned_inbox_count")
|
||||
}
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestInboxAssignmentPolicy_ChatwootRoutes() {
|
||||
policy := &model.AssignmentPolicy{AccountID: 1, Name: "Default", Enabled: true}
|
||||
s.Require().NoError(s.db.Create(policy).Error)
|
||||
inbox := &model.Inbox{AccountID: 1, Name: "Support", ChannelType: "web_widget"}
|
||||
s.Require().NoError(s.db.Create(inbox).Error)
|
||||
|
||||
body := `{"assignment_policy_id":` + uintToStr(policy.ID) + `}`
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/inboxes/"+uintToStr(inbox.ID)+"/assignment_policy", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/api/v1/accounts/1/inboxes/"+uintToStr(inbox.ID)+"/assignment_policy", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var show map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &show))
|
||||
assert.Equal(s.T(), "Default", show["name"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/api/v1/accounts/1/assignment_policies/"+uintToStr(policy.ID)+"/inboxes", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var inboxes map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &inboxes))
|
||||
s.Require().Len(inboxes["inboxes"], 1)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", "/api/v1/accounts/1/inboxes/"+uintToStr(inbox.ID)+"/assignment_policy", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func (s *AssignmentPolicyHandlerTestSuite) TestDeleteAccountPolicy_NotFound() {
|
||||
@@ -317,4 +370,4 @@ func (s *AssignmentPolicyHandlerTestSuite) TestDeleteAccountPolicy_NotFound() {
|
||||
// uintToStr converts a uint to string for URL path construction.
|
||||
func uintToStr(id uint) string {
|
||||
return strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gochat/gochat/internal/autoassignment"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
)
|
||||
|
||||
// AssignmentPolicyRepo implements GORM repository for AssignmentPolicy.
|
||||
@@ -20,13 +20,13 @@ func NewAssignmentPolicyRepo(db *gorm.DB) *AssignmentPolicyRepo {
|
||||
}
|
||||
|
||||
// Create creates a new assignment policy.
|
||||
func (r *AssignmentPolicyRepo) Create(ctx context.Context, policy *autoassignment.AssignmentPolicy) error {
|
||||
func (r *AssignmentPolicyRepo) Create(ctx context.Context, policy *model.AssignmentPolicy) error {
|
||||
return r.db.WithContext(ctx).Create(policy).Error
|
||||
}
|
||||
|
||||
// FindByID retrieves an assignment policy by primary key.
|
||||
func (r *AssignmentPolicyRepo) FindByID(ctx context.Context, id uint) (*autoassignment.AssignmentPolicy, error) {
|
||||
var policy autoassignment.AssignmentPolicy
|
||||
func (r *AssignmentPolicyRepo) FindByID(ctx context.Context, id uint) (*model.AssignmentPolicy, error) {
|
||||
var policy model.AssignmentPolicy
|
||||
if err := r.db.WithContext(ctx).First(&policy, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -34,15 +34,28 @@ func (r *AssignmentPolicyRepo) FindByID(ctx context.Context, id uint) (*autoassi
|
||||
}
|
||||
|
||||
// GetByID retrieves an assignment policy by primary key (alias for FindByID matching service expectations).
|
||||
func (r *AssignmentPolicyRepo) GetByID(ctx context.Context, id uint) (*autoassignment.AssignmentPolicy, error) {
|
||||
func (r *AssignmentPolicyRepo) GetByID(ctx context.Context, id uint) (*model.AssignmentPolicy, error) {
|
||||
return r.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
// FindByAccount retrieves the active assignment policy for an account.
|
||||
func (r *AssignmentPolicyRepo) FindByAccount(ctx context.Context, accountID uint) (*autoassignment.AssignmentPolicy, error) {
|
||||
var policy autoassignment.AssignmentPolicy
|
||||
// FindByAccount retrieves all assignment policies for an account.
|
||||
func (r *AssignmentPolicyRepo) FindByAccount(ctx context.Context, accountID uint) ([]model.AssignmentPolicy, error) {
|
||||
var policies []model.AssignmentPolicy
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("account_id = ? AND active = ?", accountID, true).
|
||||
Where("account_id = ?", accountID).
|
||||
Order("id ASC").
|
||||
Find(&policies).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policies, nil
|
||||
|
||||
}
|
||||
|
||||
// FindByAccountAndID retrieves an account-scoped assignment policy.
|
||||
func (r *AssignmentPolicyRepo) FindByAccountAndID(ctx context.Context, accountID, id uint) (*model.AssignmentPolicy, error) {
|
||||
var policy model.AssignmentPolicy
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("account_id = ? AND id = ?", accountID, id).
|
||||
First(&policy).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,13 +63,24 @@ func (r *AssignmentPolicyRepo) FindByAccount(ctx context.Context, accountID uint
|
||||
}
|
||||
|
||||
// Update updates an assignment policy.
|
||||
func (r *AssignmentPolicyRepo) Update(ctx context.Context, policy *autoassignment.AssignmentPolicy) error {
|
||||
func (r *AssignmentPolicyRepo) Update(ctx context.Context, policy *model.AssignmentPolicy) error {
|
||||
return r.db.WithContext(ctx).Save(policy).Error
|
||||
}
|
||||
|
||||
// Delete soft-deletes an assignment policy.
|
||||
func (r *AssignmentPolicyRepo) Delete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&autoassignment.AssignmentPolicy{}, id).Error
|
||||
return r.db.WithContext(ctx).Delete(&model.AssignmentPolicy{}, id).Error
|
||||
|
||||
}
|
||||
|
||||
// CountAssignedInboxes returns how many inboxes use an assignment policy.
|
||||
func (r *AssignmentPolicyRepo) CountAssignedInboxes(ctx context.Context, policyID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.InboxAssignmentPolicy{}).
|
||||
Where("assignment_policy_id = ?", policyID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// InboxAssignmentPolicyRepo implements GORM repository for InboxAssignmentPolicy.
|
||||
@@ -71,13 +95,13 @@ func NewInboxAssignmentPolicyRepo(db *gorm.DB) *InboxAssignmentPolicyRepo {
|
||||
}
|
||||
|
||||
// Create creates a new inbox assignment policy.
|
||||
func (r *InboxAssignmentPolicyRepo) Create(ctx context.Context, policy *autoassignment.InboxAssignmentPolicy) error {
|
||||
func (r *InboxAssignmentPolicyRepo) Create(ctx context.Context, policy *model.InboxAssignmentPolicy) error {
|
||||
return r.db.WithContext(ctx).Create(policy).Error
|
||||
}
|
||||
|
||||
// FindByID retrieves an inbox assignment policy by primary key.
|
||||
func (r *InboxAssignmentPolicyRepo) FindByID(ctx context.Context, id uint) (*autoassignment.InboxAssignmentPolicy, error) {
|
||||
var policy autoassignment.InboxAssignmentPolicy
|
||||
func (r *InboxAssignmentPolicyRepo) FindByID(ctx context.Context, id uint) (*model.InboxAssignmentPolicy, error) {
|
||||
var policy model.InboxAssignmentPolicy
|
||||
if err := r.db.WithContext(ctx).First(&policy, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,22 +109,70 @@ func (r *InboxAssignmentPolicyRepo) FindByID(ctx context.Context, id uint) (*aut
|
||||
}
|
||||
|
||||
// FindByInbox retrieves the active assignment policy override for an inbox.
|
||||
func (r *InboxAssignmentPolicyRepo) FindByInbox(ctx context.Context, accountID, inboxID uint) (*autoassignment.InboxAssignmentPolicy, error) {
|
||||
var policy autoassignment.InboxAssignmentPolicy
|
||||
func (r *InboxAssignmentPolicyRepo) FindByInbox(ctx context.Context, accountID, inboxID uint) (*model.InboxAssignmentPolicy, error) {
|
||||
var policy model.InboxAssignmentPolicy
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("account_id = ? AND inbox_id = ? AND active = ?", accountID, inboxID, true).
|
||||
Joins("JOIN assignment_policies ON assignment_policies.id = inbox_assignment_policies.assignment_policy_id").
|
||||
Where("assignment_policies.account_id = ? AND inbox_assignment_policies.inbox_id = ?", accountID, inboxID).
|
||||
First(&policy).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
// FindPolicyByInbox retrieves the assignment policy associated with an inbox.
|
||||
func (r *InboxAssignmentPolicyRepo) FindPolicyByInbox(ctx context.Context, accountID, inboxID uint) (*model.AssignmentPolicy, error) {
|
||||
var policy model.AssignmentPolicy
|
||||
if err := r.db.WithContext(ctx).
|
||||
Joins("JOIN inbox_assignment_policies ON inbox_assignment_policies.assignment_policy_id = assignment_policies.id").
|
||||
Where("assignment_policies.account_id = ? AND inbox_assignment_policies.inbox_id = ?", accountID, inboxID).
|
||||
First(&policy).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
// FindInboxesByPolicy retrieves inboxes linked to an account-scoped policy.
|
||||
func (r *InboxAssignmentPolicyRepo) FindInboxesByPolicy(ctx context.Context, accountID, policyID uint) ([]model.Inbox, error) {
|
||||
var inboxes []model.Inbox
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN inbox_assignment_policies ON inbox_assignment_policies.inbox_id = inboxes.id").
|
||||
Joins("JOIN assignment_policies ON assignment_policies.id = inbox_assignment_policies.assignment_policy_id").
|
||||
Where("assignment_policies.account_id = ? AND assignment_policies.id = ?", accountID, policyID).
|
||||
Order("inboxes.id ASC").
|
||||
Find(&inboxes).Error
|
||||
return inboxes, err
|
||||
}
|
||||
|
||||
// ReplaceForInbox attaches one policy to an inbox, replacing an old association.
|
||||
func (r *InboxAssignmentPolicyRepo) ReplaceForInbox(ctx context.Context, inboxID, policyID uint) (*model.InboxAssignmentPolicy, error) {
|
||||
var policy model.InboxAssignmentPolicy
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("inbox_id = ?", inboxID).Delete(&model.InboxAssignmentPolicy{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
policy = model.InboxAssignmentPolicy{InboxID: inboxID, AssignmentPolicyID: policyID}
|
||||
return tx.Create(&policy).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
// DeleteByInbox removes an inbox assignment policy association.
|
||||
func (r *InboxAssignmentPolicyRepo) DeleteByInbox(ctx context.Context, inboxID uint) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("inbox_id = ?", inboxID).
|
||||
Delete(&model.InboxAssignmentPolicy{}).Error
|
||||
}
|
||||
|
||||
// Update updates an inbox assignment policy.
|
||||
func (r *InboxAssignmentPolicyRepo) Update(ctx context.Context, policy *autoassignment.InboxAssignmentPolicy) error {
|
||||
func (r *InboxAssignmentPolicyRepo) Update(ctx context.Context, policy *model.InboxAssignmentPolicy) error {
|
||||
return r.db.WithContext(ctx).Save(policy).Error
|
||||
}
|
||||
|
||||
// Delete soft-deletes an inbox assignment policy.
|
||||
func (r *InboxAssignmentPolicyRepo) Delete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&autoassignment.InboxAssignmentPolicy{}, id).Error
|
||||
return r.db.WithContext(ctx).Delete(&model.InboxAssignmentPolicy{}, id).Error
|
||||
}
|
||||
|
||||
@@ -1683,23 +1683,29 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
|
||||
// Reference: Chatwoot resources :assignment_policies
|
||||
assignmentPolicies := accountScoped.Group("/assignment_policies")
|
||||
{
|
||||
assignmentPolicies.GET("/", h.AssignmentPolicy.GetAccountPolicy)
|
||||
assignmentPolicies.GET("", h.AssignmentPolicy.ListAccountPolicies)
|
||||
assignmentPolicies.GET("/", h.AssignmentPolicy.ListAccountPolicies)
|
||||
assignmentPolicies.POST("", h.AssignmentPolicy.CreateAccountPolicy)
|
||||
assignmentPolicies.POST("/", h.AssignmentPolicy.CreateAccountPolicy)
|
||||
assignmentPolicies.GET("/:policy_id", h.AssignmentPolicy.GetAccountPolicy)
|
||||
assignmentPolicies.PUT("/:policy_id", h.AssignmentPolicy.UpdateAccountPolicy)
|
||||
assignmentPolicies.PATCH("/:policy_id", h.AssignmentPolicy.UpdateAccountPolicy)
|
||||
assignmentPolicies.DELETE("/:policy_id", h.AssignmentPolicy.DeleteAccountPolicy)
|
||||
// Nested inboxes (Chatwoot: resources :assignment_policy_inboxes)
|
||||
assignmentPolicies.GET("/:policy_id/inboxes", h.AssignmentPolicy.GetInboxPolicy)
|
||||
assignmentPolicies.POST("/:policy_id/inboxes", h.AssignmentPolicy.CreateInboxPolicy)
|
||||
assignmentPolicies.DELETE("/:policy_id/inboxes/:inbox_id", h.AssignmentPolicy.DeleteInboxPolicy)
|
||||
assignmentPolicies.GET("/:policy_id/inboxes", h.AssignmentPolicy.ListPolicyInboxes)
|
||||
assignmentPolicies.POST("/:policy_id/inboxes", h.AssignmentPolicy.AddPolicyInbox)
|
||||
assignmentPolicies.DELETE("/:policy_id/inboxes/:inbox_id", h.AssignmentPolicy.RemovePolicyInbox)
|
||||
}
|
||||
|
||||
// Inbox-level assignment policy overrides (nested under inboxes)
|
||||
inboxAssignment := accountScoped.Group("/inboxes/:inbox_id/assignment_policy")
|
||||
{
|
||||
inboxAssignment.GET("/", h.AssignmentPolicyV2.GetInboxPolicy)
|
||||
inboxAssignment.POST("/", h.AssignmentPolicyV2.SetInboxPolicy)
|
||||
inboxAssignment.DELETE("/", h.AssignmentPolicyV2.DeleteInboxPolicy)
|
||||
inboxAssignment.GET("", h.AssignmentPolicy.GetInboxPolicy)
|
||||
inboxAssignment.GET("/", h.AssignmentPolicy.GetInboxPolicy)
|
||||
inboxAssignment.POST("", h.AssignmentPolicy.CreateInboxPolicy)
|
||||
inboxAssignment.POST("/", h.AssignmentPolicy.CreateInboxPolicy)
|
||||
inboxAssignment.DELETE("", h.AssignmentPolicy.DeleteCurrentInboxPolicy)
|
||||
inboxAssignment.DELETE("/", h.AssignmentPolicy.DeleteCurrentInboxPolicy)
|
||||
inboxAssignment.PUT("/:policy_id", h.AssignmentPolicy.UpdateInboxPolicy)
|
||||
inboxAssignment.DELETE("/:policy_id", h.AssignmentPolicy.DeleteInboxPolicy)
|
||||
}
|
||||
|
||||
@@ -2,22 +2,28 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/autoassignment"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AssignmentPolicyService implements business logic for AssignmentPolicy operations.
|
||||
// Reference: Chatwoot app/controllers/api/v1/assignment_policies_controller.rb
|
||||
type AssignmentPolicyService struct {
|
||||
policyRepo *repository.AssignmentPolicyRepo
|
||||
inboxPolicyRepo *repository.InboxAssignmentPolicyRepo
|
||||
assignSvc *autoassignment.AssignmentService
|
||||
policyRepo *repository.AssignmentPolicyRepo
|
||||
inboxPolicyRepo *repository.InboxAssignmentPolicyRepo
|
||||
assignSvc *autoassignment.AssignmentService
|
||||
}
|
||||
|
||||
type AssignmentPolicyPayload map[string]any
|
||||
|
||||
// NewAssignmentPolicyService creates a new AssignmentPolicy service.
|
||||
func NewAssignmentPolicyService(
|
||||
policyRepo *repository.AssignmentPolicyRepo,
|
||||
@@ -29,73 +35,105 @@ func NewAssignmentPolicyService(
|
||||
|
||||
// CreatePolicyRequest is the DTO for creating an assignment policy.
|
||||
type CreatePolicyRequest struct {
|
||||
Policy string `json:"policy" validate:"required,oneof=round_robin longest_waiting lowest_load"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AssignmentOrder int `json:"assignment_order,omitempty"`
|
||||
ConversationPriority int `json:"conversation_priority,omitempty"`
|
||||
FairDistributionLimit int `json:"fair_distribution_limit,omitempty"`
|
||||
FairDistributionWindow int `json:"fair_distribution_window,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
// UpdatePolicyRequest is the DTO for updating an assignment policy.
|
||||
type UpdatePolicyRequest struct {
|
||||
Policy string `json:"policy,omitempty" validate:"omitempty,oneof=round_robin longest_waiting lowest_load"`
|
||||
FairDistributionLimit int `json:"fair_distribution_limit,omitempty"`
|
||||
FairDistributionWindow int `json:"fair_distribution_window,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AssignmentOrder *int `json:"assignment_order,omitempty"`
|
||||
ConversationPriority *int `json:"conversation_priority,omitempty"`
|
||||
FairDistributionLimit *int `json:"fair_distribution_limit,omitempty"`
|
||||
FairDistributionWindow *int `json:"fair_distribution_window,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
// CreateInboxPolicyRequest is the DTO for creating an inbox assignment policy.
|
||||
type CreateInboxPolicyRequest struct {
|
||||
InboxID uint `json:"inbox_id" validate:"required"`
|
||||
Policy string `json:"policy" validate:"required,oneof=round_robin longest_waiting lowest_load"`
|
||||
FairDistributionLimit int `json:"fair_distribution_limit,omitempty"`
|
||||
FairDistributionWindow int `json:"fair_distribution_window,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
InboxID uint `json:"inbox_id" validate:"required"`
|
||||
AssignmentPolicyID uint `json:"assignment_policy_id" validate:"required"`
|
||||
}
|
||||
|
||||
// UpdateInboxPolicyRequest is the DTO for updating an inbox assignment policy.
|
||||
type UpdateInboxPolicyRequest struct {
|
||||
Policy string `json:"policy,omitempty" validate:"omitempty,oneof=round_robin longest_waiting lowest_load"`
|
||||
FairDistributionLimit int `json:"fair_distribution_limit,omitempty"`
|
||||
FairDistributionWindow int `json:"fair_distribution_window,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
AssignmentPolicyID uint `json:"assignment_policy_id" validate:"required"`
|
||||
}
|
||||
|
||||
// GetAccountPolicy retrieves the account-level assignment policy.
|
||||
func (s *AssignmentPolicyService) GetAccountPolicy(ctx context.Context, accountID uint) (*autoassignment.AssignmentPolicy, error) {
|
||||
policy, err := s.policyRepo.FindByAccount(ctx, accountID)
|
||||
// ListAccountPolicies retrieves all account-scoped assignment policies.
|
||||
func (s *AssignmentPolicyService) ListAccountPolicies(ctx context.Context, accountID uint) ([]AssignmentPolicyPayload, error) {
|
||||
policies, err := s.policyRepo.FindByAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list assignment policies: %w", err)
|
||||
}
|
||||
|
||||
items := make([]AssignmentPolicyPayload, 0, len(policies))
|
||||
for i := range policies {
|
||||
payload, err := s.SerializePolicy(ctx, &policies[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, payload)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetAccountPolicy retrieves an account-scoped assignment policy by ID.
|
||||
func (s *AssignmentPolicyService) GetAccountPolicy(ctx context.Context, accountID uint, policyID ...uint) (*model.AssignmentPolicy, error) {
|
||||
if len(policyID) > 0 && policyID[0] != 0 {
|
||||
policy, err := s.policyRepo.FindByAccountAndID(ctx, accountID, policyID[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
policies, err := s.policyRepo.FindByAccount(ctx, accountID)
|
||||
if err != nil || len(policies) == 0 {
|
||||
if err == nil {
|
||||
err = gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
||||
}
|
||||
return policy, nil
|
||||
return &policies[0], nil
|
||||
}
|
||||
|
||||
// CreateAccountPolicy creates the account-level assignment policy.
|
||||
func (s *AssignmentPolicyService) CreateAccountPolicy(ctx context.Context, accountID uint, req CreatePolicyRequest) (*autoassignment.AssignmentPolicy, error) {
|
||||
func (s *AssignmentPolicyService) CreateAccountPolicy(ctx context.Context, accountID uint, req CreatePolicyRequest) (*model.AssignmentPolicy, error) {
|
||||
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
active := true
|
||||
if req.Active != nil {
|
||||
active = *req.Active
|
||||
enabled := true
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
|
||||
limit := 5
|
||||
limit := 100
|
||||
if req.FairDistributionLimit > 0 {
|
||||
limit = req.FairDistributionLimit
|
||||
}
|
||||
|
||||
window := 300
|
||||
window := 3600
|
||||
if req.FairDistributionWindow > 0 {
|
||||
window = req.FairDistributionWindow
|
||||
}
|
||||
|
||||
policy := &autoassignment.AssignmentPolicy{
|
||||
policy := &model.AssignmentPolicy{
|
||||
AccountID: accountID,
|
||||
Policy: autoassignment.AssignmentPolicyType(req.Policy),
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
AssignmentOrder: req.AssignmentOrder,
|
||||
ConversationPriority: req.ConversationPriority,
|
||||
FairDistributionLimit: limit,
|
||||
FairDistributionWindow: window,
|
||||
Active: active,
|
||||
Enabled: enabled,
|
||||
}
|
||||
|
||||
if err := s.policyRepo.Create(ctx, policy); err != nil {
|
||||
@@ -106,30 +144,34 @@ func (s *AssignmentPolicyService) CreateAccountPolicy(ctx context.Context, accou
|
||||
}
|
||||
|
||||
// UpdateAccountPolicy updates the account-level assignment policy.
|
||||
func (s *AssignmentPolicyService) UpdateAccountPolicy(ctx context.Context, id, accountID uint, req UpdatePolicyRequest) (*autoassignment.AssignmentPolicy, error) {
|
||||
func (s *AssignmentPolicyService) UpdateAccountPolicy(ctx context.Context, id, accountID uint, req UpdatePolicyRequest) (*model.AssignmentPolicy, error) {
|
||||
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policy, err := s.policyRepo.FindByID(ctx, id)
|
||||
policy, err := s.policyRepo.FindByAccountAndID(ctx, accountID, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
||||
}
|
||||
if policy.AccountID != accountID {
|
||||
return nil, fmt.Errorf("assignment policy does not belong to account")
|
||||
}
|
||||
|
||||
if req.Policy != "" {
|
||||
policy.Policy = autoassignment.AssignmentPolicyType(req.Policy)
|
||||
if req.Name != "" {
|
||||
policy.Name = req.Name
|
||||
}
|
||||
if req.FairDistributionLimit > 0 {
|
||||
policy.FairDistributionLimit = req.FairDistributionLimit
|
||||
policy.Description = req.Description
|
||||
if req.AssignmentOrder != nil {
|
||||
policy.AssignmentOrder = *req.AssignmentOrder
|
||||
}
|
||||
if req.FairDistributionWindow > 0 {
|
||||
policy.FairDistributionWindow = req.FairDistributionWindow
|
||||
if req.ConversationPriority != nil {
|
||||
policy.ConversationPriority = *req.ConversationPriority
|
||||
}
|
||||
if req.Active != nil {
|
||||
policy.Active = *req.Active
|
||||
if req.FairDistributionLimit != nil {
|
||||
policy.FairDistributionLimit = *req.FairDistributionLimit
|
||||
}
|
||||
if req.FairDistributionWindow != nil {
|
||||
policy.FairDistributionWindow = *req.FairDistributionWindow
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
policy.Enabled = *req.Enabled
|
||||
}
|
||||
|
||||
if err := s.policyRepo.Update(ctx, policy); err != nil {
|
||||
@@ -141,13 +183,10 @@ func (s *AssignmentPolicyService) UpdateAccountPolicy(ctx context.Context, id, a
|
||||
|
||||
// DeleteAccountPolicy deletes the account-level assignment policy.
|
||||
func (s *AssignmentPolicyService) DeleteAccountPolicy(ctx context.Context, id, accountID uint) error {
|
||||
policy, err := s.policyRepo.FindByID(ctx, id)
|
||||
_, err := s.policyRepo.FindByAccountAndID(ctx, accountID, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assignment policy not found: %w", err)
|
||||
}
|
||||
if policy.AccountID != accountID {
|
||||
return fmt.Errorf("assignment policy does not belong to account")
|
||||
}
|
||||
|
||||
if err := s.policyRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete assignment policy: %w", err)
|
||||
@@ -155,102 +194,90 @@ func (s *AssignmentPolicyService) DeleteAccountPolicy(ctx context.Context, id, a
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInboxPolicy retrieves the inbox-level assignment policy override.
|
||||
func (s *AssignmentPolicyService) GetInboxPolicy(ctx context.Context, accountID, inboxID uint) (*autoassignment.InboxAssignmentPolicy, error) {
|
||||
policy, err := s.inboxPolicyRepo.FindByInbox(ctx, accountID, inboxID)
|
||||
// GetInboxPolicy retrieves the assignment policy associated with an inbox.
|
||||
func (s *AssignmentPolicyService) GetInboxPolicy(ctx context.Context, accountID, inboxID uint) (*model.AssignmentPolicy, error) {
|
||||
policy, err := s.inboxPolicyRepo.FindPolicyByInbox(ctx, accountID, inboxID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inbox assignment policy not found: %w", err)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// ListPolicyInboxes retrieves inboxes associated with an assignment policy.
|
||||
func (s *AssignmentPolicyService) ListPolicyInboxes(ctx context.Context, accountID, policyID uint) ([]model.Inbox, error) {
|
||||
if _, err := s.policyRepo.FindByAccountAndID(ctx, accountID, policyID); err != nil {
|
||||
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
||||
}
|
||||
return s.inboxPolicyRepo.FindInboxesByPolicy(ctx, accountID, policyID)
|
||||
}
|
||||
|
||||
// CreateInboxPolicy creates an inbox-level assignment policy override.
|
||||
func (s *AssignmentPolicyService) CreateInboxPolicy(ctx context.Context, accountID uint, req CreateInboxPolicyRequest) (*autoassignment.InboxAssignmentPolicy, error) {
|
||||
func (s *AssignmentPolicyService) CreateInboxPolicy(ctx context.Context, accountID uint, req CreateInboxPolicyRequest) (*model.AssignmentPolicy, error) {
|
||||
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
active := true
|
||||
if req.Active != nil {
|
||||
active = *req.Active
|
||||
if _, err := s.policyRepo.FindByAccountAndID(ctx, accountID, req.AssignmentPolicyID); err != nil {
|
||||
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
||||
}
|
||||
|
||||
limit := 5
|
||||
if req.FairDistributionLimit > 0 {
|
||||
limit = req.FairDistributionLimit
|
||||
if _, err := s.inboxPolicyRepo.ReplaceForInbox(ctx, req.InboxID, req.AssignmentPolicyID); err != nil {
|
||||
return nil, fmt.Errorf("create inbox assignment policy: %w", err)
|
||||
}
|
||||
|
||||
window := 300
|
||||
if req.FairDistributionWindow > 0 {
|
||||
window = req.FairDistributionWindow
|
||||
}
|
||||
|
||||
policy := &autoassignment.InboxAssignmentPolicy{
|
||||
AccountID: accountID,
|
||||
InboxID: req.InboxID,
|
||||
Policy: autoassignment.AssignmentPolicyType(req.Policy),
|
||||
FairDistributionLimit: limit,
|
||||
FairDistributionWindow: window,
|
||||
Active: active,
|
||||
}
|
||||
|
||||
if err := s.inboxPolicyRepo.Create(ctx, policy); err != nil {
|
||||
applogger.L().Errorf("failed to create inbox assignment policy: %v", err)
|
||||
return nil, fmt.Errorf("failed to create inbox assignment policy: %w", err)
|
||||
}
|
||||
return policy, nil
|
||||
return s.GetInboxPolicy(ctx, accountID, req.InboxID)
|
||||
}
|
||||
|
||||
// UpdateInboxPolicy updates an inbox-level assignment policy override.
|
||||
func (s *AssignmentPolicyService) UpdateInboxPolicy(ctx context.Context, id, accountID uint, req UpdateInboxPolicyRequest) (*autoassignment.InboxAssignmentPolicy, error) {
|
||||
func (s *AssignmentPolicyService) UpdateInboxPolicy(ctx context.Context, id, accountID uint, req UpdateInboxPolicyRequest) (*model.AssignmentPolicy, error) {
|
||||
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policy, err := s.inboxPolicyRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inbox assignment policy not found: %w", err)
|
||||
}
|
||||
if policy.AccountID != accountID {
|
||||
return nil, fmt.Errorf("inbox assignment policy does not belong to account")
|
||||
}
|
||||
|
||||
if req.Policy != "" {
|
||||
policy.Policy = autoassignment.AssignmentPolicyType(req.Policy)
|
||||
}
|
||||
if req.FairDistributionLimit > 0 {
|
||||
policy.FairDistributionLimit = req.FairDistributionLimit
|
||||
}
|
||||
if req.FairDistributionWindow > 0 {
|
||||
policy.FairDistributionWindow = req.FairDistributionWindow
|
||||
}
|
||||
if req.Active != nil {
|
||||
policy.Active = *req.Active
|
||||
}
|
||||
|
||||
if err := s.inboxPolicyRepo.Update(ctx, policy); err != nil {
|
||||
applogger.L().Errorf("failed to update inbox assignment policy: %v", err)
|
||||
return nil, fmt.Errorf("failed to update inbox assignment policy: %w", err)
|
||||
}
|
||||
return policy, nil
|
||||
return s.CreateInboxPolicy(ctx, accountID, CreateInboxPolicyRequest{InboxID: id, AssignmentPolicyID: req.AssignmentPolicyID})
|
||||
}
|
||||
|
||||
// DeleteInboxPolicy deletes an inbox-level assignment policy override.
|
||||
func (s *AssignmentPolicyService) DeleteInboxPolicy(ctx context.Context, id, accountID uint) error {
|
||||
policy, err := s.inboxPolicyRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if _, err := s.inboxPolicyRepo.FindPolicyByInbox(ctx, accountID, id); err != nil {
|
||||
return fmt.Errorf("inbox assignment policy not found: %w", err)
|
||||
}
|
||||
if policy.AccountID != accountID {
|
||||
return fmt.Errorf("inbox assignment policy does not belong to account")
|
||||
}
|
||||
|
||||
if err := s.inboxPolicyRepo.Delete(ctx, id); err != nil {
|
||||
if err := s.inboxPolicyRepo.DeleteByInbox(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete inbox assignment policy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SerializePolicy returns Chatwoot assignment_policy partial shape.
|
||||
func (s *AssignmentPolicyService) SerializePolicy(ctx context.Context, policy *model.AssignmentPolicy) (AssignmentPolicyPayload, error) {
|
||||
count, err := s.policyRepo.CountAssignedInboxes(ctx, policy.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count assigned inboxes: %w", err)
|
||||
}
|
||||
return AssignmentPolicyPayload{
|
||||
"id": policy.ID,
|
||||
"name": policy.Name,
|
||||
"description": policy.Description,
|
||||
"assignment_order": policy.AssignmentOrder,
|
||||
"conversation_priority": policy.ConversationPriority,
|
||||
"fair_distribution_limit": policy.FairDistributionLimit,
|
||||
"fair_distribution_window": policy.FairDistributionWindow,
|
||||
"enabled": policy.Enabled,
|
||||
"assigned_inbox_count": count,
|
||||
"created_at": unixSeconds(policy.CreatedAt),
|
||||
"updated_at": unixSeconds(policy.UpdatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func unixSeconds(value time.Time) int64 {
|
||||
if value.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return value.Unix()
|
||||
}
|
||||
|
||||
func IsAssignmentPolicyNotFound(err error) bool {
|
||||
return err != nil && (errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, gorm.ErrInvalidDB))
|
||||
}
|
||||
|
||||
// AssignConversation assigns a specific conversation to an agent using auto-assignment.
|
||||
func (s *AssignmentPolicyService) AssignConversation(ctx context.Context, conversationID, inboxID, accountID uint) (uint, error) {
|
||||
agentID, err := s.assignSvc.AssignConversation(ctx, conversationID, inboxID, accountID)
|
||||
|
||||
Reference in New Issue
Block a user