feat(crm): persist contact data imports

This commit is contained in:
2026-06-05 03:20:34 +08:00
parent ab2ca840fb
commit 8eed2f1943
10 changed files with 289 additions and 46 deletions
+3 -3
View File
@@ -724,18 +724,18 @@ func (h *ContactHandler) Import(c *gin.Context) {
file, _, fileErr = c.Request.FormFile("file")
}
if fileErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "csv file required"})
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to import contacts"})
return
}
defer file.Close()
result, svcErr := h.svc.ImportCSV(c.Request.Context(), accountID, file)
_, svcErr := h.svc.ImportContacts(c.Request.Context(), accountID, getUserID(c), file)
if svcErr != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to import contacts"})
return
}
c.JSON(http.StatusOK, result)
c.Status(http.StatusOK)
}
// ContactableInboxes returns inboxes that a contact can be associated with.
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
@@ -51,6 +52,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
&model.Contact{},
&model.Tag{},
&model.ContactLabel{},
&model.DataImport{},
&model.Conversation{},
&model.Message{},
&model.ContactInbox{},
@@ -82,6 +84,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.POST("/api/v1/accounts/:id/contacts/import", s.handler.Import)
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Get)
s.router.POST("/api/v1/accounts/:id/contacts", s.handler.Create)
s.router.PUT("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Update)
@@ -120,6 +123,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupTest() {
s.db.Exec("DELETE FROM contact_notes")
s.db.Exec("DELETE FROM contact_labels")
s.db.Exec("DELETE FROM tags")
s.db.Exec("DELETE FROM data_imports")
s.db.Exec("DELETE FROM messages")
s.db.Exec("DELETE FROM notes")
s.db.Exec("DELETE FROM contact_inboxes")
@@ -597,6 +601,40 @@ func (s *ContactHandlerCRUDTestSuite) TestMerge_ChatwootActionsPathReturnsRawCon
s.Equal(int64(1), count)
}
func (s *ContactHandlerCRUDTestSuite) TestImport_CreatesDataImportAndReturnsOK() {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("import_file", "contacts.csv")
s.Require().NoError(err)
_, err = part.Write([]byte("name,email,labels\nImported,imported@example.com,vip\n"))
s.Require().NoError(err)
s.Require().NoError(writer.Close())
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST",
fmt.Sprintf("/api/v1/accounts/%d/contacts/import", s.account.ID),
&body)
req.Header.Set("Content-Type", writer.FormDataContentType())
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
s.Empty(w.Body.String())
var dataImport model.DataImport
s.Require().NoError(s.db.Where("account_id = ? AND data_type = ?", s.account.ID, "contacts").First(&dataImport).Error)
s.Equal(string(model.DataImportStatusCompleted), dataImport.Status)
s.Equal(1, dataImport.TotalRecords)
s.Equal(1, dataImport.ProcessedRecords)
s.Require().NotNil(dataImport.UserID)
s.Equal(s.user.ID, *dataImport.UserID)
var contact model.Contact
s.Require().NoError(s.db.Where("account_id = ? AND email = ?", s.account.ID, "imported@example.com").First(&contact).Error)
var labelCount int64
s.db.Model(&model.ContactLabel{}).Where("account_id = ? AND contact_id = ?", s.account.ID, contact.ID).Count(&labelCount)
s.Equal(int64(1), labelCount)
}
func (s *ContactHandlerCRUDTestSuite) TestLabels_UpdateListAndFilter() {
bodyBytes, _ := json.Marshal(map[string]interface{}{"labels": []string{"vip", "trial"}})
@@ -89,8 +89,8 @@ func TestContactImportMissingFile(t *testing.T) {
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/contacts/import", nil)
router.ServeHTTP(w, req)
// Without a multipart form, FormFile will error, so we expect 400
assert.Equal(t, http.StatusBadRequest, w.Code)
// Chatwoot returns 422 when import_file is missing.
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Contains(t, resp, "error")
@@ -138,4 +138,4 @@ func TestDeleteCustomAttributesBadContactID(t *testing.T) {
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
}