feat(conversations): align draft message route
This commit is contained in:
@@ -19,6 +19,98 @@ func NewDraftMessageHandler(draftSvc *service.DraftMessageService) *DraftMessage
|
||||
return &DraftMessageHandler{draftSvc: draftSvc}
|
||||
}
|
||||
|
||||
// Show retrieves the Chatwoot conversation-scoped draft message.
|
||||
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages
|
||||
func (h *DraftMessageHandler) Show(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
|
||||
conversationID, err := parseUintParam(c, "conversation_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
|
||||
return
|
||||
}
|
||||
|
||||
draft, svcErr := h.draftSvc.ShowConversationDraft(c.Request.Context(), accountID, conversationID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
if draft == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"has_draft": false})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"has_draft": true, "message": draft.Content})
|
||||
}
|
||||
|
||||
// UpdateConversationDraft upserts the Chatwoot conversation-scoped draft message.
|
||||
// PATCH/PUT /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages
|
||||
func (h *DraftMessageHandler) UpdateConversationDraft(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
|
||||
conversationID, err := parseUintParam(c, "conversation_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DraftMessage struct {
|
||||
Message *string `json:"message"`
|
||||
} `json:"draft_message"`
|
||||
Message *string `json:"message"`
|
||||
Content *string `json:"content"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
message := ""
|
||||
if req.DraftMessage.Message != nil {
|
||||
message = *req.DraftMessage.Message
|
||||
} else if req.Message != nil {
|
||||
message = *req.Message
|
||||
} else if req.Content != nil {
|
||||
message = *req.Content
|
||||
}
|
||||
|
||||
if svcErr := h.draftSvc.SetConversationDraft(c.Request.Context(), accountID, conversationID, getUserID(c), message); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteConversationDraft deletes the Chatwoot conversation-scoped draft message.
|
||||
// DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages
|
||||
func (h *DraftMessageHandler) DeleteConversationDraft(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
|
||||
conversationID, err := parseUintParam(c, "conversation_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.draftSvc.DeleteConversationDraft(c.Request.Context(), accountID, conversationID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// List retrieves all draft messages for a conversation (filtered by current user).
|
||||
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages
|
||||
func (h *DraftMessageHandler) List(c *gin.Context) {
|
||||
|
||||
@@ -90,8 +90,12 @@ func (s *DraftMessageHandlerTestSuite) SetupSuite() {
|
||||
{
|
||||
drafts := convGroup.Group("/draft_messages")
|
||||
{
|
||||
drafts.GET("", handler.List)
|
||||
drafts.POST("", handler.Create)
|
||||
drafts.GET("", handler.Show)
|
||||
drafts.PATCH("", handler.UpdateConversationDraft)
|
||||
drafts.PUT("", handler.UpdateConversationDraft)
|
||||
drafts.DELETE("", handler.DeleteConversationDraft)
|
||||
drafts.GET("/", handler.List)
|
||||
drafts.POST("/", handler.Create)
|
||||
drafts.GET("/:draft_id", handler.Get)
|
||||
drafts.PATCH("/:draft_id", handler.Update)
|
||||
drafts.DELETE("/:draft_id", handler.Delete)
|
||||
@@ -106,8 +110,12 @@ func makeDraftURL(accountID, convID uint) string {
|
||||
"/draft_messages"
|
||||
}
|
||||
|
||||
func makeDraftCollectionURL(accountID, convID uint) string {
|
||||
return makeDraftURL(accountID, convID) + "/"
|
||||
}
|
||||
|
||||
func makeDraftURLWithID(accountID, convID, draftID uint) string {
|
||||
return makeDraftURL(accountID, convID) + "/" + strconv.FormatUint(uint64(draftID), 10)
|
||||
return makeDraftCollectionURL(accountID, convID) + strconv.FormatUint(uint64(draftID), 10)
|
||||
}
|
||||
|
||||
func (s *DraftMessageHandlerTestSuite) Test_CreateDraft() {
|
||||
@@ -117,7 +125,7 @@ func (s *DraftMessageHandlerTestSuite) Test_CreateDraft() {
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", makeDraftURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req, _ := http.NewRequest("POST", makeDraftCollectionURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
@@ -132,6 +140,83 @@ func (s *DraftMessageHandlerTestSuite) Test_CreateDraft() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DraftMessageHandlerTestSuite) Test_ShowDraft_ChatwootShapeWithoutDraft() {
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM draft_messages").Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", makeDraftURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(s.T(), false, resp["has_draft"])
|
||||
assert.NotContains(s.T(), resp, "data")
|
||||
}
|
||||
|
||||
func (s *DraftMessageHandlerTestSuite) Test_UpdateAndShowDraft_ChatwootShape() {
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM draft_messages").Error)
|
||||
body := map[string]interface{}{"draft_message": map[string]interface{}{"message": "Saved draft"}}
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PATCH", makeDraftURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", makeDraftURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(s.T(), true, resp["has_draft"])
|
||||
assert.Equal(s.T(), "Saved draft", resp["message"])
|
||||
assert.NotContains(s.T(), resp, "success")
|
||||
}
|
||||
|
||||
func (s *DraftMessageHandlerTestSuite) Test_DeleteDraft_ChatwootShape() {
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM draft_messages").Error)
|
||||
s.Require().NoError(s.db.Create(&model.DraftMessage{ConversationID: s.testConv.ID, UserID: s.testUser.ID, Content: "Delete me"}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", makeDraftURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", makeDraftURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(s.T(), false, resp["has_draft"])
|
||||
}
|
||||
|
||||
func (s *DraftMessageHandlerTestSuite) Test_ShowDraft_UsesDisplayIDRoute() {
|
||||
s.Require().NoError(s.db.Exec("DELETE FROM draft_messages").Error)
|
||||
displayID := uint(91)
|
||||
s.testConv.DisplayID = &displayID
|
||||
s.Require().NoError(s.db.Save(s.testConv).Error)
|
||||
s.Require().NoError(s.db.Create(&model.DraftMessage{ConversationID: s.testConv.ID, UserID: s.testUser.ID, Content: "Display draft"}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", makeDraftURL(s.testAccount.ID, displayID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(s.T(), true, resp["has_draft"])
|
||||
assert.Equal(s.T(), "Display draft", resp["message"])
|
||||
}
|
||||
|
||||
func (s *DraftMessageHandlerTestSuite) Test_ListDrafts() {
|
||||
// First create a draft
|
||||
body := map[string]interface{}{
|
||||
@@ -140,13 +225,13 @@ func (s *DraftMessageHandlerTestSuite) Test_ListDrafts() {
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", makeDraftURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req, _ := http.NewRequest("POST", makeDraftCollectionURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Now list
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", makeDraftURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
req, _ = http.NewRequest("GET", makeDraftCollectionURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
@@ -160,7 +245,7 @@ func (s *DraftMessageHandlerTestSuite) Test_GetDraft() {
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", makeDraftURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req, _ := http.NewRequest("POST", makeDraftCollectionURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
@@ -170,7 +255,7 @@ func (s *DraftMessageHandlerTestSuite) Test_GetDraft() {
|
||||
// Try to get the draft by ID - note: the response structure may vary
|
||||
// We just verify the list endpoint works for round-trip verification
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", makeDraftURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
req, _ = http.NewRequest("GET", makeDraftCollectionURL(s.testAccount.ID, s.testConv.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
@@ -184,7 +269,7 @@ func (s *DraftMessageHandlerTestSuite) Test_DeleteDraft() {
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", makeDraftURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req, _ := http.NewRequest("POST", makeDraftCollectionURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
@@ -213,7 +298,7 @@ func (s *DraftMessageHandlerTestSuite) Test_UpdateDraft() {
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", makeDraftURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req, _ := http.NewRequest("POST", makeDraftCollectionURL(s.testAccount.ID, s.testConv.ID), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
@@ -247,7 +332,7 @@ func (s *DraftMessageHandlerTestSuite) Test_CreateDraft_InvalidAccountID() {
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/1/draft_messages", bytes.NewReader(b))
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/1/draft_messages/", bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user