feat(contacts): align active pagination payloads

This commit is contained in:
2026-06-06 01:18:24 +08:00
parent 7425e269d7
commit c76c25f2ea
3 changed files with 75 additions and 17 deletions
+12 -11
View File
@@ -25,6 +25,8 @@ type ContactHandler struct {
conversationSvc *service.ConversationService
}
const chatwootContactResultsPerPage = 15
// NewContactHandler creates a new ContactHandler.
func NewContactHandler(svc *service.ContactService, contactInboxSvc *service.ContactInboxService, mergeSvc *service.ContactMergeService, contactNoteSvc *service.ContactNoteService, conversationSvc ...*service.ConversationService) *ContactHandler {
h := &ContactHandler{svc: svc, contactInboxSvc: contactInboxSvc, mergeSvc: mergeSvc, contactNoteSvc: contactNoteSvc}
@@ -60,7 +62,7 @@ func (h *ContactHandler) List(c *gin.Context) {
}
page := getPage(c)
perPage := getPageSize(c)
perPage := chatwootContactResultsPerPage
offset := (page - 1) * perPage
sort := c.DefaultQuery("sort", "")
@@ -106,7 +108,7 @@ func (h *ContactHandler) Search(c *gin.Context) {
return
}
page := getPage(c)
perPage := getPageSize(c)
perPage := chatwootContactResultsPerPage
offset := (page - 1) * perPage
sort := c.DefaultQuery("sort", "")
searchMode := search.ParseSearchMode(c.DefaultQuery("search_mode", ""))
@@ -676,7 +678,7 @@ func (h *ContactHandler) DeleteContactInbox(c *gin.Context) {
}
// Active retrieves contacts with recent activity.
// GET /api/v1/accounts/:id/contacts/active?sort=name&page=1&page_size=25
// GET /api/v1/accounts/:id/contacts/active?sort=name&page=1
// Reference: Chatwoot contacts#active
func (h *ContactHandler) Active(c *gin.Context) {
accountID := parseAccountIDParam(c)
@@ -686,7 +688,7 @@ func (h *ContactHandler) Active(c *gin.Context) {
}
page := getPage(c)
perPage := getPageSize(c)
perPage := chatwootContactResultsPerPage
offset := (page - 1) * perPage
sort := c.DefaultQuery("sort", "")
if !h.svc.Ready() {
@@ -700,10 +702,7 @@ func (h *ContactHandler) Active(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"contacts": contacts,
"meta": gin.H{"count": total, "page": page, "page_size": perPage},
})
c.JSON(http.StatusOK, contactListResponse(c.Request.Context(), h.svc.DB(), contacts, total, page, includeContactInboxes(c), nil))
}
// Export downloads contacts as CSV.
@@ -932,14 +931,16 @@ func (h *ContactHandler) Filter(c *gin.Context) {
return
}
p := pagination.Parse(c)
contacts, total, svcErr := h.svc.Filter(c.Request.Context(), accountID, params, p.Offset, p.PerPage)
page := getPage(c)
perPage := chatwootContactResultsPerPage
offset := (page - 1) * perPage
contacts, total, svcErr := h.svc.Filter(c.Request.Context(), accountID, params, offset, perPage)
if svcErr != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to filter contacts"})
return
}
c.JSON(http.StatusOK, contactListResponse(c.Request.Context(), h.svc.DB(), contacts, total, p.Page, includeContactInboxes(c), nil))
c.JSON(http.StatusOK, contactListResponse(c.Request.Context(), h.svc.DB(), contacts, total, page, includeContactInboxes(c), nil))
}
// DestroyCustomAttributes removes all custom attributes from a contact.
@@ -87,6 +87,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
s.router.Use(gin.Recovery(), s.mockAuthMiddleware())
s.router.GET("/api/v1/accounts/:id/contacts", s.handler.List)
s.router.GET("/api/v1/accounts/:id/contacts/search", s.handler.Search)
s.router.GET("/api/v1/accounts/:id/contacts/active", s.handler.Active)
s.router.POST("/api/v1/accounts/:id/contacts/export", s.handler.ExportRequest)
s.router.GET("/api/v1/accounts/:id/contacts/export/:export_id/download", s.handler.DownloadExport)
s.router.POST("/api/v1/accounts/:id/contacts/import", s.handler.Import)
@@ -100,6 +101,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/labels", s.handler.ListLabels)
s.router.POST("/api/v1/accounts/:id/contacts/:contact_id/labels", s.handler.UpdateLabels)
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/contact_inboxes", s.handler.ListContactInboxes)
s.router.POST("/api/v1/accounts/:id/contacts/:contact_id/destroy_custom_attributes", s.handler.DestroyCustomAttributes)
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.ListNotes)
s.router.POST("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.CreateNote)
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/notes/:note_id", s.handler.ShowNote)
@@ -233,7 +235,7 @@ func (s *ContactHandlerCRUDTestSuite) TestList_InvalidAccountID() {
func (s *ContactHandlerCRUDTestSuite) TestList_Pagination() {
// Create additional contacts
for i := 0; i < 3; i++ {
for i := 0; i < 16; i++ {
c := &model.Contact{
AccountID: s.account.ID,
Name: fmt.Sprintf("Contact %d", i),
@@ -243,7 +245,7 @@ func (s *ContactHandlerCRUDTestSuite) TestList_Pagination() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET",
fmt.Sprintf("/api/v1/accounts/%d/contacts?page=1&page_size=2", s.account.ID), nil)
fmt.Sprintf("/api/v1/accounts/%d/contacts?page=2&page_size=2", s.account.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
@@ -254,11 +256,41 @@ func (s *ContactHandlerCRUDTestSuite) TestList_Pagination() {
meta, ok := resp["meta"]
s.True(ok)
metaMap := meta.(map[string]interface{})
s.Equal(float64(4), metaMap["count"]) // 1 original + 3 new
s.Equal(float64(17), metaMap["count"]) // 1 original + 16 new
s.Equal(float64(2), metaMap["current_page"])
payload := resp["payload"].([]interface{})
s.Len(payload, 2)
}
func (s *ContactHandlerCRUDTestSuite) TestActive_ChatwootPayloadAndPageSize() {
now := time.Now().Unix()
for i := 0; i < 16; i++ {
c := &model.Contact{
AccountID: s.account.ID,
Name: fmt.Sprintf("Active Contact %d", i),
Email: fmt.Sprintf("active-%d@example.com", i),
LastActivityAt: &now,
}
s.Require().NoError(s.db.Create(c).Error)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET",
fmt.Sprintf("/api/v1/accounts/%d/contacts/active?page=1&page_size=2", s.account.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.NotContains(resp, "contacts")
meta := resp["meta"].(map[string]interface{})
s.Equal(float64(16), meta["count"])
s.Equal(float64(1), meta["current_page"])
payload := resp["payload"].([]interface{})
s.Len(payload, 15)
}
// ===========================
// Search Contacts
// ===========================
@@ -777,6 +809,29 @@ func (s *ContactHandlerCRUDTestSuite) TestLabels_UpdateListAndFilter() {
s.Equal(float64(s.contact.ID), payload[0].(map[string]interface{})["id"])
}
func (s *ContactHandlerCRUDTestSuite) TestDestroyCustomAttributes_SelectedKeysPayload() {
attrs := model.JSONMap{"tier": "gold", "plan": "pro", "vip": true}
s.contact.CustomAttributes = model.ToDatatypesJSON(&attrs)
s.Require().NoError(s.db.Save(s.contact).Error)
bodyBytes, _ := json.Marshal(map[string]interface{}{"custom_attributes": []string{"plan", "vip"}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST",
fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/destroy_custom_attributes", s.account.ID, s.contact.ID),
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
payload := resp["payload"].(map[string]interface{})
customAttrs := payload["custom_attributes"].(map[string]interface{})
s.Equal(map[string]interface{}{"tier": "gold"}, customAttrs)
s.Contains(payload, "contact_inboxes")
}
func (s *ContactHandlerCRUDTestSuite) TestDelete_InvalidAccountID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE",