feat(settings): align agents teams payloads
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
@@ -44,7 +45,8 @@ func (h *AgentHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OKWithMeta(c, agents, pg.Page, pg.PerPage, total)
|
||||
_ = total
|
||||
c.JSON(http.StatusOK, serializeAgentDetails(agents, accountID))
|
||||
}
|
||||
|
||||
// Get returns a single agent by ID.
|
||||
@@ -69,7 +71,7 @@ func (h *AgentHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, agent)
|
||||
c.JSON(http.StatusOK, serializeAgentDetail(agent, accountID))
|
||||
}
|
||||
|
||||
// Create adds an agent to the account.
|
||||
@@ -88,15 +90,11 @@ func (h *AgentHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Chatwoot: params.require(:agent) → request body must be {"agent": {...}}
|
||||
var wrapper struct {
|
||||
Agent service.CreateAgentRequest `json:"agent"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&wrapper); err != nil {
|
||||
var req service.CreateAgentRequest
|
||||
if err := bindJSONWrappedOrRaw(c, "agent", &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
req := wrapper.Agent
|
||||
|
||||
// Chatwoot: validate_limit → can_add_agent? — returns 402 if limit exceeded
|
||||
canAdd, err := h.svc.CanAddAgent(c.Request.Context(), accountID)
|
||||
@@ -121,7 +119,7 @@ func (h *AgentHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, agent)
|
||||
c.JSON(http.StatusOK, serializeAgentDetail(agent, accountID))
|
||||
}
|
||||
|
||||
// Update modifies agent details (name on User, role/availability on AccountUser).
|
||||
@@ -140,15 +138,11 @@ func (h *AgentHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Chatwoot: params.require(:agent) → request body must be {"agent": {...}}
|
||||
var wrapper struct {
|
||||
Agent service.UpdateAgentRequest `json:"agent"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&wrapper); err != nil {
|
||||
var req service.UpdateAgentRequest
|
||||
if err := bindJSONWrappedOrRaw(c, "agent", &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
req := wrapper.Agent
|
||||
|
||||
agent, svcErr := h.svc.Update(c.Request.Context(), uint(id), accountID, req)
|
||||
if svcErr != nil {
|
||||
@@ -157,7 +151,7 @@ func (h *AgentHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, agent)
|
||||
c.JSON(http.StatusOK, serializeAgentDetail(agent, accountID))
|
||||
}
|
||||
|
||||
// Delete removes an agent from the account.
|
||||
@@ -183,7 +177,7 @@ func (h *AgentHandler) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{"message": "agent removed"})
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// BulkCreate adds multiple agents to the account by email.
|
||||
@@ -220,12 +214,63 @@ func (h *AgentHandler) BulkCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
agents, svcErr := h.svc.BulkCreate(c.Request.Context(), accountID, userID, req)
|
||||
_, svcErr := h.svc.BulkCreate(c.Request.Context(), accountID, userID, req)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("BulkCreate agents for account %d: %v", accountID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, agents)
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func serializeAgentDetails(agents []repository.AgentDetail, accountID uint) []map[string]any {
|
||||
payload := make([]map[string]any, 0, len(agents))
|
||||
for i := range agents {
|
||||
payload = append(payload, serializeAgentDetail(&agents[i], accountID))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeAgentDetail(agent *repository.AgentDetail, accountID uint) map[string]any {
|
||||
if agent == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
payload := serializeAgentUser(&agent.User, accountID, agent.Role, agent.Availability, agent.AutoOffline, agent.CustomRoleID)
|
||||
if agent.InvitedBy != 0 {
|
||||
payload["invited_by"] = agent.InvitedBy
|
||||
}
|
||||
if agent.AccountUserID != 0 {
|
||||
payload["account_user_id"] = agent.AccountUserID
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeAgentUser(user *model.User, accountID uint, role string, availability string, autoOffline bool, customRoleID uint) map[string]any {
|
||||
if user == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
availabilityStatus := nonEmpty(availability, availabilityStatus(user.Available))
|
||||
payload := map[string]any{
|
||||
"id": user.ID,
|
||||
"account_id": accountID,
|
||||
"availability_status": availabilityStatus,
|
||||
"auto_offline": autoOffline,
|
||||
"confirmed": user.ConfirmedAt != nil,
|
||||
"email": user.Email,
|
||||
"provider": nonEmpty(user.Provider, "email"),
|
||||
"available_name": nonEmpty(user.DisplayName, user.Name),
|
||||
"name": user.Name,
|
||||
"role": nonEmpty(role, user.Role),
|
||||
"thumbnail": user.AvatarURL,
|
||||
}
|
||||
if attrs := jsonObject(user.CustomAttributes); len(attrs) > 0 {
|
||||
payload["custom_attributes"] = attrs
|
||||
}
|
||||
if customRoleID != 0 {
|
||||
payload["custom_role_id"] = customRoleID
|
||||
} else if user.CustomRoleID != nil && *user.CustomRoleID != 0 {
|
||||
payload["custom_role_id"] = *user.CustomRoleID
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -78,15 +77,7 @@ func (s *AgentHandlerTestSuite) makeRequest(method, path string, body interface{
|
||||
|
||||
var bodyBytes []byte
|
||||
if body != nil {
|
||||
// Chatwoot params.require(:agent) → wrap body under "agent" key for POST/PUT/PATCH
|
||||
// Exception: bulk_create does NOT use params.require
|
||||
needsWrap := (method == "POST" || method == "PUT" || method == "PATCH") && !strings.Contains(path, "bulk_create")
|
||||
if needsWrap {
|
||||
wrapped := map[string]interface{}{"agent": body}
|
||||
bodyBytes, _ = json.Marshal(wrapped)
|
||||
} else {
|
||||
bodyBytes, _ = json.Marshal(body)
|
||||
}
|
||||
bodyBytes, _ = json.Marshal(body)
|
||||
}
|
||||
c.Request = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
|
||||
if body != nil {
|
||||
@@ -117,29 +108,30 @@ func (s *AgentHandlerTestSuite) TestListEmpty() {
|
||||
s.handler.List(c)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
data := resp["data"].([]interface{})
|
||||
var data []interface{}
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &data))
|
||||
assert.Equal(s.T(), 0, len(data))
|
||||
}
|
||||
|
||||
func (s *AgentHandlerTestSuite) TestCreateAgent() {
|
||||
customRoleID := uint(7)
|
||||
req := service.CreateAgentRequest{
|
||||
Email: "agent1@test.com",
|
||||
Name: "Agent One",
|
||||
Role: "agent",
|
||||
Email: "agent1@test.com",
|
||||
Name: "Agent One",
|
||||
Role: "agent",
|
||||
CustomRoleID: &customRoleID,
|
||||
}
|
||||
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
|
||||
s.handler.Create(c)
|
||||
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
assert.Equal(s.T(), "agent1@test.com", data["email"])
|
||||
assert.Equal(s.T(), "Agent One", data["name"])
|
||||
assert.Equal(s.T(), "offline", data["availability_status"])
|
||||
assert.Equal(s.T(), float64(7), data["custom_role_id"])
|
||||
assert.Contains(s.T(), data, "confirmed")
|
||||
}
|
||||
|
||||
func (s *AgentHandlerTestSuite) TestCreateAgentDuplicate() {
|
||||
@@ -151,7 +143,7 @@ func (s *AgentHandlerTestSuite) TestCreateAgentDuplicate() {
|
||||
}
|
||||
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
|
||||
s.handler.Create(c)
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
// Try creating again — should conflict
|
||||
w2, c2 := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
|
||||
@@ -178,11 +170,10 @@ func (s *AgentHandlerTestSuite) TestGetAgent() {
|
||||
}
|
||||
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
|
||||
s.handler.Create(c)
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var createResp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &createResp)
|
||||
data := createResp["data"].(map[string]interface{})
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
agentID := uint(data["id"].(float64))
|
||||
|
||||
// Get the agent
|
||||
@@ -191,10 +182,8 @@ func (s *AgentHandlerTestSuite) TestGetAgent() {
|
||||
s.handler.Get(c2)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w2.Code)
|
||||
var getResp map[string]interface{}
|
||||
json.Unmarshal(w2.Body.Bytes(), &getResp)
|
||||
assert.True(s.T(), getResp["success"].(bool))
|
||||
getData := getResp["data"].(map[string]interface{})
|
||||
var getData map[string]interface{}
|
||||
json.Unmarshal(w2.Body.Bytes(), &getData)
|
||||
assert.Equal(s.T(), "agent3@test.com", getData["email"])
|
||||
}
|
||||
|
||||
@@ -213,17 +202,16 @@ func (s *AgentHandlerTestSuite) TestUpdateAgent() {
|
||||
}
|
||||
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
|
||||
s.handler.Create(c)
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var createResp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &createResp)
|
||||
data := createResp["data"].(map[string]interface{})
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
agentID := uint(data["id"].(float64))
|
||||
|
||||
// Update the agent
|
||||
updateReq := service.UpdateAgentRequest{
|
||||
Name: "Updated Name",
|
||||
Role: "administrator",
|
||||
Name: "Updated Name",
|
||||
Role: "administrator",
|
||||
Availability: "online",
|
||||
}
|
||||
w2, c2 := s.makeRequest("PUT", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), updateReq, s.account.ID, s.user.ID)
|
||||
@@ -231,13 +219,11 @@ func (s *AgentHandlerTestSuite) TestUpdateAgent() {
|
||||
s.handler.Update(c2)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w2.Code)
|
||||
var updateResp map[string]interface{}
|
||||
json.Unmarshal(w2.Body.Bytes(), &updateResp)
|
||||
assert.True(s.T(), updateResp["success"].(bool))
|
||||
updateData := updateResp["data"].(map[string]interface{})
|
||||
var updateData map[string]interface{}
|
||||
json.Unmarshal(w2.Body.Bytes(), &updateData)
|
||||
assert.Equal(s.T(), "Updated Name", updateData["name"])
|
||||
assert.Equal(s.T(), "administrator", updateData["role"])
|
||||
assert.Equal(s.T(), "online", updateData["availability"])
|
||||
assert.Equal(s.T(), "online", updateData["availability_status"])
|
||||
}
|
||||
|
||||
func (s *AgentHandlerTestSuite) TestDeleteAgent() {
|
||||
@@ -249,11 +235,10 @@ func (s *AgentHandlerTestSuite) TestDeleteAgent() {
|
||||
}
|
||||
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
|
||||
s.handler.Create(c)
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var createResp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &createResp)
|
||||
data := createResp["data"].(map[string]interface{})
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
agentID := uint(data["id"].(float64))
|
||||
|
||||
// Delete the agent
|
||||
@@ -278,18 +263,7 @@ func (s *AgentHandlerTestSuite) TestBulkCreate() {
|
||||
s.handler.BulkCreate(c)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.True(s.T(), resp["success"].(bool))
|
||||
data := resp["data"].([]interface{})
|
||||
assert.Equal(s.T(), 3, len(data))
|
||||
|
||||
// Verify emails
|
||||
for i, agent := range data {
|
||||
agentMap := agent.(map[string]interface{})
|
||||
expectedEmail := fmt.Sprintf("bulk%d@test.com", i+1)
|
||||
assert.Equal(s.T(), expectedEmail, agentMap["email"])
|
||||
}
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
}
|
||||
|
||||
func (s *AgentHandlerTestSuite) TestBulkCreateValidation() {
|
||||
@@ -310,7 +284,7 @@ func (s *AgentHandlerTestSuite) TestBulkCreateSkipsDuplicates() {
|
||||
}
|
||||
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", preReq, s.account.ID, s.user.ID)
|
||||
s.handler.Create(c)
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
// Bulk create with a duplicate + a new one
|
||||
req := service.BulkCreateAgentRequest{
|
||||
@@ -320,13 +294,7 @@ func (s *AgentHandlerTestSuite) TestBulkCreateSkipsDuplicates() {
|
||||
s.handler.BulkCreate(c2)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w2.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w2.Body.Bytes(), &resp)
|
||||
data := resp["data"].([]interface{})
|
||||
// Only the new one should be in results (existing skipped silently)
|
||||
assert.Equal(s.T(), 1, len(data))
|
||||
newAgent := data[0].(map[string]interface{})
|
||||
assert.Equal(s.T(), "new@test.com", newAgent["email"])
|
||||
assert.Empty(s.T(), w2.Body.String())
|
||||
}
|
||||
|
||||
func (s *AgentHandlerTestSuite) TestListAfterCreate() {
|
||||
@@ -339,7 +307,7 @@ func (s *AgentHandlerTestSuite) TestListAfterCreate() {
|
||||
}
|
||||
w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID)
|
||||
s.handler.Create(c)
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
// List agents
|
||||
@@ -347,9 +315,8 @@ func (s *AgentHandlerTestSuite) TestListAfterCreate() {
|
||||
s.handler.List(c)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
data := resp["data"].([]interface{})
|
||||
var data []interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &data)
|
||||
assert.Equal(s.T(), 2, len(data))
|
||||
}
|
||||
|
||||
@@ -360,4 +327,4 @@ func (s *AgentHandlerTestSuite) TestListNoAccountID() {
|
||||
// No account_id set
|
||||
s.handler.List(c)
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func bindJSONWrappedOrRaw(c *gin.Context, wrapperKey string, target any) error {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
if len(bytes.TrimSpace(body)) == 0 {
|
||||
return fmt.Errorf("empty request body")
|
||||
}
|
||||
|
||||
var wrapper map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &wrapper); err == nil {
|
||||
if raw, ok := wrapper[wrapperKey]; ok && len(raw) > 0 && string(raw) != "null" {
|
||||
return json.Unmarshal(raw, target)
|
||||
}
|
||||
}
|
||||
return json.Unmarshal(body, target)
|
||||
}
|
||||
|
||||
// parseUintParam extracts a uint path parameter from the Gin context.
|
||||
func parseUintParam(c *gin.Context, param string) (uint, error) {
|
||||
val := c.Param(param)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -40,7 +41,8 @@ func (h *TeamHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OKWithMeta(c, teams, pg.Page, pg.PerPage, total)
|
||||
_ = total
|
||||
c.JSON(http.StatusOK, serializeTeams(c, h.svc, teams))
|
||||
}
|
||||
|
||||
// Get returns a single team by ID.
|
||||
@@ -52,7 +54,7 @@ func (h *TeamHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
id, err := parseTeamIDParam(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID")
|
||||
return
|
||||
@@ -65,7 +67,7 @@ func (h *TeamHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, team)
|
||||
c.JSON(http.StatusOK, serializeTeamForAccount(c, h.svc, team))
|
||||
}
|
||||
|
||||
// Create creates a new team within an account.
|
||||
@@ -77,15 +79,11 @@ func (h *TeamHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Chatwoot: params.require(:team) → request body must be {"team": {...}}
|
||||
var wrapper struct {
|
||||
Team service.CreateTeamRequest `json:"team"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&wrapper); err != nil {
|
||||
var req service.CreateTeamRequest
|
||||
if err := bindJSONWrappedOrRaw(c, "team", &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
req := wrapper.Team
|
||||
|
||||
team, svcErr := h.svc.Create(c.Request.Context(), accountID, req)
|
||||
if svcErr != nil {
|
||||
@@ -94,7 +92,7 @@ func (h *TeamHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, team)
|
||||
c.JSON(http.StatusOK, serializeTeamForAccount(c, h.svc, team))
|
||||
}
|
||||
|
||||
// Update updates an existing team.
|
||||
@@ -106,21 +104,17 @@ func (h *TeamHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
id, err := parseTeamIDParam(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Chatwoot: params.require(:team) → request body must be {"team": {...}}
|
||||
var wrapper struct {
|
||||
Team service.UpdateTeamRequest `json:"team"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&wrapper); err != nil {
|
||||
var req service.UpdateTeamRequest
|
||||
if err := bindJSONWrappedOrRaw(c, "team", &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
req := wrapper.Team
|
||||
|
||||
team, svcErr := h.svc.Update(c.Request.Context(), uint(id), accountID, req)
|
||||
if svcErr != nil {
|
||||
@@ -129,7 +123,7 @@ func (h *TeamHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, team)
|
||||
c.JSON(http.StatusOK, serializeTeamForAccount(c, h.svc, team))
|
||||
}
|
||||
|
||||
// Delete soft-deletes a team.
|
||||
@@ -141,7 +135,7 @@ func (h *TeamHandler) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
id, err := parseTeamIDParam(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID")
|
||||
return
|
||||
@@ -153,7 +147,7 @@ func (h *TeamHandler) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(c)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// AddMembers adds users to a team.
|
||||
@@ -165,7 +159,7 @@ func (h *TeamHandler) AddMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
id, err := parseTeamIDParam(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID")
|
||||
return
|
||||
@@ -177,14 +171,19 @@ func (h *TeamHandler) AddMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
members, svcErr := h.svc.AddMembers(c.Request.Context(), uint(id), accountID, req.UserIDs)
|
||||
_, svcErr := h.svc.AddMembers(c.Request.Context(), uint(id), accountID, req.UserIDs)
|
||||
if svcErr != nil {
|
||||
applogger.L().Errorf("Add members to team %d for account %d: %v", id, accountID, svcErr)
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
members, svcErr := h.svc.ListMembers(c.Request.Context(), uint(id), accountID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, members)
|
||||
c.JSON(http.StatusOK, serializeTeamMemberAgents(accountID, members))
|
||||
}
|
||||
|
||||
// RemoveMembers removes a user from a team.
|
||||
@@ -196,7 +195,7 @@ func (h *TeamHandler) RemoveMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
id, err := parseTeamIDParam(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID")
|
||||
return
|
||||
@@ -214,7 +213,7 @@ func (h *TeamHandler) RemoveMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(c)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// ListMembers retrieves all members of a team.
|
||||
@@ -226,7 +225,7 @@ func (h *TeamHandler) ListMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
id, err := parseTeamIDParam(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID")
|
||||
return
|
||||
@@ -239,7 +238,7 @@ func (h *TeamHandler) ListMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, members)
|
||||
c.JSON(http.StatusOK, serializeTeamMemberAgents(accountID, members))
|
||||
}
|
||||
|
||||
// UpdateMembers adds/removes members to match the provided user_ids list.
|
||||
@@ -252,7 +251,7 @@ func (h *TeamHandler) UpdateMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
teamID, err := strconv.ParseUint(c.Param("team_id"), 10, 32)
|
||||
teamID, err := parseTeamIDParam(c)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID")
|
||||
return
|
||||
@@ -271,5 +270,51 @@ func (h *TeamHandler) UpdateMembers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, members)
|
||||
c.JSON(http.StatusOK, serializeTeamMemberAgents(accountID, members))
|
||||
}
|
||||
|
||||
func parseTeamIDParam(c *gin.Context) (uint64, error) {
|
||||
value := c.Param("team_id")
|
||||
if value == "" {
|
||||
value = c.Param("id")
|
||||
}
|
||||
return strconv.ParseUint(value, 10, 32)
|
||||
}
|
||||
|
||||
func serializeTeams(c *gin.Context, svc *service.TeamService, teams []model.Team) []map[string]any {
|
||||
payload := make([]map[string]any, 0, len(teams))
|
||||
for i := range teams {
|
||||
payload = append(payload, serializeTeamForAccount(c, svc, &teams[i]))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeTeamForAccount(c *gin.Context, svc *service.TeamService, team *model.Team) map[string]any {
|
||||
if team == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
payload := map[string]any{
|
||||
"id": team.ID,
|
||||
"name": team.Name,
|
||||
"description": team.Description,
|
||||
"allow_auto_assign": team.AllowAutoAssignment,
|
||||
"account_id": team.AccountID,
|
||||
"is_member": false,
|
||||
}
|
||||
userID := getUserID(c)
|
||||
if userID != 0 && svc != nil && svc.DB() != nil {
|
||||
var count int64
|
||||
svc.DB().WithContext(c.Request.Context()).Model(&model.TeamMember{}).Where("team_id = ? AND user_id = ?", team.ID, userID).Count(&count)
|
||||
payload["is_member"] = count > 0
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func serializeTeamMemberAgents(accountID uint, members []model.TeamMember) []map[string]any {
|
||||
payload := make([]map[string]any, 0, len(members))
|
||||
for i := range members {
|
||||
member := members[i]
|
||||
payload = append(payload, serializeAgentUser(&member.User, accountID, "", member.AvailabilityStatus, false, 0))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -74,6 +75,8 @@ func (s *TeamHandlerTestSuite) TestList_Success() {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload []map[string]any
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
}
|
||||
|
||||
func (s *TeamHandlerTestSuite) TestCreate_Success() {
|
||||
@@ -84,12 +87,16 @@ func (s *TeamHandlerTestSuite) TestCreate_Success() {
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"team":{"name":"test-team","description":"a test team"}}`
|
||||
body := `{"name":"test-team","description":"a test team","allow_auto_assign":false}`
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/teams", s.account.ID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusCreated, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload map[string]any
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
assert.Equal(s.T(), "test-team", payload["name"])
|
||||
assert.Equal(s.T(), false, payload["allow_auto_assign"])
|
||||
}
|
||||
|
||||
func (s *TeamHandlerTestSuite) TestGet_Success() {
|
||||
@@ -106,6 +113,27 @@ func (s *TeamHandlerTestSuite) TestGet_Success() {
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/teams/%d", s.account.ID, team.ID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload map[string]any
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
assert.Equal(s.T(), "get-test-team", payload["name"])
|
||||
assert.Contains(s.T(), payload, "is_member")
|
||||
}
|
||||
|
||||
func (s *TeamHandlerTestSuite) TestGet_TeamIDParamSuccess() {
|
||||
team := &model.Team{AccountID: s.account.ID, Name: "team-id-param"}
|
||||
s.Require().NoError(s.db.Create(team).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/api/v1/accounts/:account_id/teams/:team_id", func(c *gin.Context) {
|
||||
s.authMiddleware(c)
|
||||
s.handler.Get(c)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/teams/%d", s.account.ID, team.ID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
@@ -120,12 +148,16 @@ func (s *TeamHandlerTestSuite) TestUpdate_Success() {
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"team":{"name":"updated-team","description":"updated description"}}`
|
||||
body := `{"team":{"name":"updated-team","description":"updated description","allow_auto_assign":false}}`
|
||||
req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/teams/%d", s.account.ID, team.ID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload map[string]any
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
assert.Equal(s.T(), "updated-team", payload["name"])
|
||||
assert.Equal(s.T(), false, payload["allow_auto_assign"])
|
||||
}
|
||||
|
||||
func (s *TeamHandlerTestSuite) TestDelete_Success() {
|
||||
@@ -142,7 +174,7 @@ func (s *TeamHandlerTestSuite) TestDelete_Success() {
|
||||
req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/teams/%d", s.account.ID, team.ID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusNoContent, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func (s *TeamHandlerTestSuite) TestGet_BadRequest_InvalidID() {
|
||||
@@ -157,4 +189,4 @@ func (s *TeamHandlerTestSuite) TestGet_BadRequest_InvalidID() {
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type AgentDetail struct {
|
||||
AutoOffline bool `json:"auto_offline"`
|
||||
InvitedBy uint `json:"invited_by"`
|
||||
AccountUserID uint `json:"account_user_id"`
|
||||
CustomRoleID uint `json:"custom_role_id,omitempty"`
|
||||
}
|
||||
|
||||
// ListByAccount retrieves all agents (users) for an account with pagination.
|
||||
@@ -92,6 +93,7 @@ func (r *AgentRepo) ListByAccount(ctx context.Context, accountID uint, offset, l
|
||||
AutoOffline: au.AutoOffline,
|
||||
InvitedBy: au.InvitedBy,
|
||||
AccountUserID: au.ID,
|
||||
CustomRoleID: au.CustomRoleID,
|
||||
}
|
||||
details = append(details, d)
|
||||
}
|
||||
@@ -121,13 +123,14 @@ func (r *AgentRepo) FindAgentByID(ctx context.Context, userID, accountID uint) (
|
||||
AutoOffline: au.AutoOffline,
|
||||
InvitedBy: au.InvitedBy,
|
||||
AccountUserID: au.ID,
|
||||
CustomRoleID: au.CustomRoleID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateAgent adds a user to an account (creates AccountUser).
|
||||
// Reference: Chatwoot agents_controller.rb#create → AgentBuilder.new.perform
|
||||
// If the user does not exist, creates the user first, then creates the AccountUser.
|
||||
func (r *AgentRepo) CreateAgent(ctx context.Context, accountID uint, inviterID uint, name, email, role, availability string, autoOffline bool) (*AgentDetail, error) {
|
||||
func (r *AgentRepo) CreateAgent(ctx context.Context, accountID uint, inviterID uint, name, email, role, availability string, autoOffline bool, customRoleID uint) (*AgentDetail, error) {
|
||||
// Find or create the user
|
||||
var user model.User
|
||||
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
|
||||
@@ -160,14 +163,15 @@ func (r *AgentRepo) CreateAgent(ctx context.Context, accountID uint, inviterID u
|
||||
|
||||
// Create AccountUser
|
||||
au := model.AccountUser{
|
||||
UserID: user.ID,
|
||||
AccountID: accountID,
|
||||
Role: role,
|
||||
UserID: user.ID,
|
||||
AccountID: accountID,
|
||||
Role: role,
|
||||
CustomRoleID: customRoleID,
|
||||
Availability: availability,
|
||||
AutoOffline: autoOffline,
|
||||
InvitedBy: inviterID,
|
||||
AutoOffline: autoOffline,
|
||||
InvitedBy: inviterID,
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Create(&au).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Select("UserID", "AccountID", "Role", "CustomRoleID", "Availability", "AutoOffline", "InvitedBy").Create(&au).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -175,14 +179,16 @@ func (r *AgentRepo) CreateAgent(ctx context.Context, accountID uint, inviterID u
|
||||
User: user,
|
||||
Role: au.Role,
|
||||
Availability: au.Availability,
|
||||
AutoOffline: au.AutoOffline,
|
||||
InvitedBy: au.InvitedBy,
|
||||
AccountUserID: au.ID,
|
||||
CustomRoleID: au.CustomRoleID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateAgent updates both the User (name) and AccountUser (role, availability, auto_offline).
|
||||
// Reference: Chatwoot agents_controller.rb#update → agent.update!(name) + current_account_user.update!(role, availability, auto_offline)
|
||||
func (r *AgentRepo) UpdateAgent(ctx context.Context, userID, accountID uint, name, role, availability string, autoOffline bool) (*AgentDetail, error) {
|
||||
func (r *AgentRepo) UpdateAgent(ctx context.Context, userID, accountID uint, name, role, availability string, autoOffline bool, customRoleID *uint, customRoleIDSet bool) (*AgentDetail, error) {
|
||||
// Update user name if provided
|
||||
if name != "" {
|
||||
if err := r.db.WithContext(ctx).
|
||||
@@ -201,6 +207,13 @@ func (r *AgentRepo) UpdateAgent(ctx context.Context, userID, accountID uint, nam
|
||||
if availability != "" {
|
||||
updates["availability"] = availability
|
||||
}
|
||||
if customRoleIDSet {
|
||||
if customRoleID == nil {
|
||||
updates["custom_role_id"] = 0
|
||||
} else {
|
||||
updates["custom_role_id"] = *customRoleID
|
||||
}
|
||||
}
|
||||
// Chatwoot always sends auto_offline (even false) via .compact filter
|
||||
// so we always update it when the request includes it
|
||||
updates["auto_offline"] = autoOffline
|
||||
@@ -259,7 +272,7 @@ func (r *AgentRepo) BulkCreateAgents(ctx context.Context, accountID uint, invite
|
||||
name = email[:atIdx]
|
||||
}
|
||||
|
||||
detail, err := r.CreateAgent(ctx, accountID, inviterID, name, email, "agent", "offline", false)
|
||||
detail, err := r.CreateAgent(ctx, accountID, inviterID, name, email, "agent", "offline", false, 0)
|
||||
if err != nil {
|
||||
// Silently skip — Chatwoot rescues ActiveRecord::RecordInvalid and logs
|
||||
continue
|
||||
@@ -288,4 +301,4 @@ func (r *AgentRepo) CountByAccount(ctx context.Context, accountID uint) (int64,
|
||||
Where("account_id = ?", accountID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (r *TeamRepo) ListByAccount(ctx context.Context, accountID uint, offset, li
|
||||
|
||||
// Create creates a new team.
|
||||
func (r *TeamRepo) Create(ctx context.Context, team *model.Team) error {
|
||||
return r.db.WithContext(ctx).Create(team).Error
|
||||
return r.db.WithContext(ctx).Select("AccountID", "Name", "Description", "AllowAutoAssignment").Create(team).Error
|
||||
}
|
||||
|
||||
// Update updates a team.
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -25,6 +26,13 @@ func NewAgentService(agentRepo *repository.AgentRepo, db *gorm.DB) *AgentService
|
||||
return &AgentService{agentRepo: agentRepo, db: db}
|
||||
}
|
||||
|
||||
func (s *AgentService) DB() *gorm.DB {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.db
|
||||
}
|
||||
|
||||
// CreateAgentRequest is the DTO for creating/adding an agent to an account.
|
||||
// Reference: Chatwoot agents_controller.rb#create → new_agent_params (email, name, role, availability, auto_offline)
|
||||
type CreateAgentRequest struct {
|
||||
@@ -33,17 +41,37 @@ type CreateAgentRequest struct {
|
||||
Role string `json:"role" validate:"omitempty,oneof=agent administrator"`
|
||||
Availability string `json:"availability" validate:"omitempty,oneof=online offline busy"`
|
||||
AutoOffline bool `json:"auto_offline"`
|
||||
CustomRoleID *uint `json:"custom_role_id,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateAgentRequest is the DTO for updating an agent.
|
||||
// Reference: Chatwoot agents_controller.rb#update → agent_params (name on User, role/availability/auto_offline on AccountUser)
|
||||
type UpdateAgentRequest struct {
|
||||
Name string `json:"name,omitempty" validate:"omitempty,min=1"`
|
||||
Role string `json:"role,omitempty" validate:"omitempty,oneof=agent administrator"`
|
||||
Availability string `json:"availability,omitempty" validate:"omitempty,oneof=online offline busy"`
|
||||
AutoOffline bool `json:"auto_offline"`
|
||||
Name string `json:"name,omitempty" validate:"omitempty,min=1"`
|
||||
Role string `json:"role,omitempty" validate:"omitempty,oneof=agent administrator"`
|
||||
Availability string `json:"availability,omitempty" validate:"omitempty,oneof=online offline busy"`
|
||||
AutoOffline bool `json:"auto_offline"`
|
||||
CustomRoleID *uint `json:"custom_role_id,omitempty"`
|
||||
customRoleSet bool
|
||||
}
|
||||
|
||||
func (r *UpdateAgentRequest) UnmarshalJSON(data []byte) error {
|
||||
type alias UpdateAgentRequest
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
var decoded alias
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
*r = UpdateAgentRequest(decoded)
|
||||
_, r.customRoleSet = raw["custom_role_id"]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r UpdateAgentRequest) CustomRoleIDSet() bool { return r.customRoleSet }
|
||||
|
||||
// BulkCreateAgentRequest is the DTO for bulk creating agents.
|
||||
// Reference: Chatwoot agents_controller.rb#bulk_create → params[:emails]
|
||||
type BulkCreateAgentRequest struct {
|
||||
@@ -76,7 +104,11 @@ func (s *AgentService) Create(ctx context.Context, accountID uint, inviterID uin
|
||||
availability = "offline"
|
||||
}
|
||||
|
||||
detail, err := s.agentRepo.CreateAgent(ctx, accountID, inviterID, req.Name, req.Email, role, availability, req.AutoOffline)
|
||||
customRoleID := uint(0)
|
||||
if req.CustomRoleID != nil {
|
||||
customRoleID = *req.CustomRoleID
|
||||
}
|
||||
detail, err := s.agentRepo.CreateAgent(ctx, accountID, inviterID, req.Name, req.Email, role, availability, req.AutoOffline, customRoleID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrAlreadyMember) {
|
||||
return nil, repository.ErrAlreadyMember
|
||||
@@ -94,7 +126,7 @@ func (s *AgentService) Update(ctx context.Context, userID, accountID uint, req U
|
||||
return nil, fmt.Errorf("validation: %w", err)
|
||||
}
|
||||
|
||||
return s.agentRepo.UpdateAgent(ctx, userID, accountID, req.Name, req.Role, req.Availability, req.AutoOffline)
|
||||
return s.agentRepo.UpdateAgent(ctx, userID, accountID, req.Name, req.Role, req.Availability, req.AutoOffline, req.CustomRoleID, req.CustomRoleIDSet())
|
||||
}
|
||||
|
||||
// Delete removes an agent from an account (deletes AccountUser, optionally deletes User).
|
||||
@@ -159,4 +191,4 @@ func (s *AgentService) CanAddAgents(ctx context.Context, accountID uint, count i
|
||||
}
|
||||
// -1 means unlimited
|
||||
return available < 0 || available >= count, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
// TeamService implements business logic for Team operations.
|
||||
// Reference: Chatwoot app/controllers/api/v1/teams_controller.rb
|
||||
type TeamService struct {
|
||||
teamRepo *repository.TeamRepo
|
||||
teamMemberRepo *repository.TeamMemberRepo
|
||||
db *gorm.DB
|
||||
teamRepo *repository.TeamRepo
|
||||
teamMemberRepo *repository.TeamMemberRepo
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewTeamService creates a new Team service.
|
||||
@@ -26,17 +26,26 @@ func NewTeamService(teamRepo *repository.TeamRepo, teamMemberRepo *repository.Te
|
||||
return &TeamService{teamRepo: teamRepo, teamMemberRepo: teamMemberRepo, db: db}
|
||||
}
|
||||
|
||||
func (s *TeamService) DB() *gorm.DB {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.db
|
||||
}
|
||||
|
||||
// CreateTeamRequest is the DTO for creating a team.
|
||||
type CreateTeamRequest struct {
|
||||
Name string `json:"name" validate:"required,min=2"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Name string `json:"name" validate:"required,min=2"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AllowAutoAssign *bool `json:"allow_auto_assign,omitempty"`
|
||||
AllowAutoAssignment *bool `json:"allow_auto_assignment,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateTeamRequest is the DTO for updating a team.
|
||||
type UpdateTeamRequest struct {
|
||||
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AllowAutoAssign *bool `json:"allow_auto_assign,omitempty"`
|
||||
AllowAutoAssignment *bool `json:"allow_auto_assignment,omitempty"`
|
||||
}
|
||||
|
||||
@@ -66,14 +75,16 @@ func (s *TeamService) Create(ctx context.Context, accountID uint, req CreateTeam
|
||||
}
|
||||
|
||||
autoAssign := true // default
|
||||
if req.AllowAutoAssignment != nil {
|
||||
if req.AllowAutoAssign != nil {
|
||||
autoAssign = *req.AllowAutoAssign
|
||||
} else if req.AllowAutoAssignment != nil {
|
||||
autoAssign = *req.AllowAutoAssignment
|
||||
}
|
||||
|
||||
team := &model.Team{
|
||||
AccountID: accountID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
AccountID: accountID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
AllowAutoAssignment: autoAssign,
|
||||
}
|
||||
|
||||
@@ -81,6 +92,12 @@ func (s *TeamService) Create(ctx context.Context, accountID uint, req CreateTeam
|
||||
applogger.L().Errorf("failed to create team: %v", err)
|
||||
return nil, fmt.Errorf("failed to create team: %w", err)
|
||||
}
|
||||
if !autoAssign {
|
||||
team.AllowAutoAssignment = false
|
||||
if err := s.db.WithContext(ctx).Model(team).Update("allow_auto_assignment", false).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update team auto assignment: %w", err)
|
||||
}
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
@@ -100,7 +117,9 @@ func (s *TeamService) Update(ctx context.Context, id, accountID uint, req Update
|
||||
}
|
||||
// Allow empty description to clear it
|
||||
team.Description = req.Description
|
||||
if req.AllowAutoAssignment != nil {
|
||||
if req.AllowAutoAssign != nil {
|
||||
team.AllowAutoAssignment = *req.AllowAutoAssign
|
||||
} else if req.AllowAutoAssignment != nil {
|
||||
team.AllowAutoAssignment = *req.AllowAutoAssignment
|
||||
}
|
||||
|
||||
@@ -140,8 +159,8 @@ func (s *TeamService) AddMembers(ctx context.Context, teamID, accountID uint, us
|
||||
continue
|
||||
}
|
||||
members = append(members, model.TeamMember{
|
||||
TeamID: teamID,
|
||||
UserID: uid,
|
||||
TeamID: teamID,
|
||||
UserID: uid,
|
||||
AvailabilityStatus: "offline",
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user