feat(crm): persist contact export artifacts
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -706,9 +707,41 @@ func (h *ContactHandler) ExportRequest(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var req service.ContactExportRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil && err != io.EOF {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to export contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, svcErr := h.svc.ExportContacts(c.Request.Context(), accountID, getUserID(c), req); svcErr != nil {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to export contacts"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// DownloadExport streams a previously generated contacts export artifact.
|
||||
func (h *ContactHandler) DownloadExport(c *gin.Context) {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account id"})
|
||||
return
|
||||
}
|
||||
exportID, err := parseUintParam(c, "export_id")
|
||||
if err != nil || exportID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid export id"})
|
||||
return
|
||||
}
|
||||
export, svcErr := h.svc.GetContactExport(c.Request.Context(), accountID, exportID)
|
||||
if svcErr != nil || len(export.CSVData) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "export not found"})
|
||||
return
|
||||
}
|
||||
c.Header("Content-Disposition", "attachment; filename="+export.FileName)
|
||||
c.Data(http.StatusOK, export.ContentType, export.CSVData)
|
||||
}
|
||||
|
||||
// Import uploads contacts from a CSV file.
|
||||
// POST /api/v1/accounts/:id/contacts/import
|
||||
// Reference: Chatwoot contacts#import
|
||||
|
||||
@@ -52,7 +52,9 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
|
||||
&model.Contact{},
|
||||
&model.Tag{},
|
||||
&model.ContactLabel{},
|
||||
&model.ContactExport{},
|
||||
&model.DataImport{},
|
||||
&model.Notification{},
|
||||
&model.Conversation{},
|
||||
&model.Message{},
|
||||
&model.ContactInbox{},
|
||||
@@ -84,6 +86,8 @@ 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/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)
|
||||
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id", s.handler.Get)
|
||||
s.router.POST("/api/v1/accounts/:id/contacts", s.handler.Create)
|
||||
@@ -123,7 +127,9 @@ 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 contact_exports")
|
||||
s.db.Exec("DELETE FROM data_imports")
|
||||
s.db.Exec("DELETE FROM notifications")
|
||||
s.db.Exec("DELETE FROM messages")
|
||||
s.db.Exec("DELETE FROM notes")
|
||||
s.db.Exec("DELETE FROM contact_inboxes")
|
||||
@@ -635,6 +641,63 @@ func (s *ContactHandlerCRUDTestSuite) TestImport_CreatesDataImportAndReturnsOK()
|
||||
s.Equal(int64(1), labelCount)
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestExportRequest_CreatesArtifactNotificationAndReturnsOK() {
|
||||
contact := &model.Contact{AccountID: s.account.ID, Name: "Exported", Email: "exported@example.com"}
|
||||
s.Require().NoError(s.db.Create(contact).Error)
|
||||
tag := &model.Tag{AccountID: s.account.ID, Name: "vip"}
|
||||
s.Require().NoError(s.db.Create(tag).Error)
|
||||
s.Require().NoError(s.db.Create(&model.ContactLabel{AccountID: s.account.ID, ContactID: contact.ID, TagID: tag.ID}).Error)
|
||||
|
||||
bodyBytes, _ := json.Marshal(map[string]any{
|
||||
"column_names": []string{"email", "labels"},
|
||||
"label": "vip",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST",
|
||||
fmt.Sprintf("/api/v1/accounts/%d/contacts/export", s.account.ID),
|
||||
bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
s.Empty(w.Body.String())
|
||||
|
||||
var export model.ContactExport
|
||||
s.Require().NoError(s.db.Where("account_id = ?", s.account.ID).First(&export).Error)
|
||||
s.Equal(string(model.DataImportStatusCompleted), export.Status)
|
||||
s.Equal(1, export.RowCount)
|
||||
s.Contains(string(export.CSVData), "email,labels")
|
||||
s.Contains(string(export.CSVData), "exported@example.com,vip")
|
||||
s.Contains(export.FileURL, fmt.Sprintf("/contacts/export/%d/download", export.ID))
|
||||
|
||||
var notification model.Notification
|
||||
s.Require().NoError(s.db.Where("user_id = ? AND notification_type = ?", s.user.ID, "contacts_export_complete").First(¬ification).Error)
|
||||
s.Equal("ContactExport", notification.PrimaryActorType)
|
||||
s.Equal(export.ID, notification.PrimaryActorID)
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestDownloadExport_ReturnsPersistedCSVArtifact() {
|
||||
export := &model.ContactExport{
|
||||
AccountID: s.account.ID,
|
||||
UserID: &s.user.ID,
|
||||
Status: string(model.DataImportStatusCompleted),
|
||||
FileName: "contacts.csv",
|
||||
ContentType: "text/csv",
|
||||
CSVData: []byte("\ufeffemail\nexported@example.com\n"),
|
||||
}
|
||||
s.Require().NoError(s.db.Create(export).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET",
|
||||
fmt.Sprintf("/api/v1/accounts/%d/contacts/export/%d/download", s.account.ID, export.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
s.Equal("text/csv", w.Header().Get("Content-Type"))
|
||||
s.Contains(w.Header().Get("Content-Disposition"), "contacts.csv")
|
||||
s.Contains(w.Body.String(), "exported@example.com")
|
||||
}
|
||||
|
||||
func (s *ContactHandlerCRUDTestSuite) TestLabels_UpdateListAndFilter() {
|
||||
bodyBytes, _ := json.Marshal(map[string]interface{}{"labels": []string{"vip", "trial"}})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user