feat(conversations): align draft message route

This commit is contained in:
2026-06-06 15:22:18 +08:00
parent 18a2cc5e83
commit 495b194411
9 changed files with 281 additions and 25 deletions
@@ -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)