feat(conversations): expose reporting events
This commit is contained in:
@@ -811,6 +811,52 @@ func (h *ConversationHandler) InboxAssistant(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"assistant": gin.H{"id": assistant.ID, "name": assistant.Name}})
|
||||
}
|
||||
|
||||
// ReportingEvents returns raw reporting events for a conversation.
|
||||
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events
|
||||
// Reference: Chatwoot enterprise conversations_controller#reporting_events.
|
||||
func (h *ConversationHandler) ReportingEvents(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
|
||||
}
|
||||
|
||||
events, svcErr := h.conversationSvc.ListReportingEvents(c.Request.Context(), accountID, conversationID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
payload := make([]gin.H, 0, len(events))
|
||||
for i := range events {
|
||||
payload = append(payload, serializeReportingEvent(events[i]))
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func serializeReportingEvent(event model.ReportingEvent) gin.H {
|
||||
return gin.H{
|
||||
"id": event.ID,
|
||||
"name": event.Name,
|
||||
"value": event.Value,
|
||||
"value_in_business_hours": event.ValueInBusinessHours,
|
||||
"event_start_time": event.EventStartTime,
|
||||
"event_end_time": event.EventEndTime,
|
||||
"account_id": event.AccountID,
|
||||
"inbox_id": event.InboxID,
|
||||
"user_id": event.UserID,
|
||||
"conversation_id": event.ConversationID,
|
||||
"created_at": event.CreatedAt,
|
||||
"updated_at": event.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToggleTyping toggles the typing status for an agent in a conversation.
|
||||
// POST /api/v1/accounts/:account_id/conversations/:id/toggle_typing_status
|
||||
// Reference: Chatwoot conversations_controller#toggle_typing_status (member route)
|
||||
|
||||
@@ -68,6 +68,7 @@ func (s *ConversationHandlerTestSuite) SetupSuite() {
|
||||
&model.ContactInbox{},
|
||||
&model.Conversation{},
|
||||
&model.ConversationParticipant{},
|
||||
&model.ReportingEvent{},
|
||||
&model.Message{},
|
||||
&model.Attachment{},
|
||||
&model.InboxMember{},
|
||||
@@ -122,6 +123,7 @@ func (s *ConversationHandlerTestSuite) SetupSuite() {
|
||||
conversations.POST("/:conversation_id/transcript", handler.Transcript)
|
||||
conversations.POST("/:conversation_id/custom_attributes", handler.UpdateCustomAttributes)
|
||||
conversations.GET("/:conversation_id/attachments", handler.ListAttachments)
|
||||
conversations.GET("/:conversation_id/reporting_events", handler.ReportingEvents)
|
||||
conversations.POST("/:conversation_id/toggle_typing", handler.ToggleTyping)
|
||||
conversations.POST("/:conversation_id/update_last_seen", handler.UpdateLastSeen)
|
||||
}
|
||||
@@ -137,6 +139,7 @@ func (s *ConversationHandlerTestSuite) TearDownSuite() {
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TearDownTest() {
|
||||
s.db.Exec("DELETE FROM attachments")
|
||||
s.db.Exec("DELETE FROM reporting_events")
|
||||
s.db.Exec("DELETE FROM conversations")
|
||||
s.db.Exec("DELETE FROM contact_inboxes")
|
||||
s.db.Exec("DELETE FROM contacts")
|
||||
@@ -504,6 +507,102 @@ func (s *ConversationHandlerTestSuite) TestListAttachmentsUsesChatwootFixedPageS
|
||||
assert.Equal(s.T(), float64(newestID), first["id"])
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestReportingEventsReturnsRawArrayInCreatedOrder() {
|
||||
conversationID := s.testConv.ID
|
||||
baseTime := time.Now().Add(-2 * time.Hour).UTC()
|
||||
newer := &model.ReportingEvent{
|
||||
Base: model.Base{CreatedAt: baseTime.Add(time.Hour), UpdatedAt: baseTime.Add(time.Hour)},
|
||||
AccountID: s.testAccount.ID,
|
||||
Name: "resolution_time",
|
||||
Value: 42,
|
||||
ValueInBusinessHours: 21,
|
||||
ConversationID: &conversationID,
|
||||
EventStartTime: baseTime,
|
||||
EventEndTime: baseTime.Add(time.Minute),
|
||||
}
|
||||
older := &model.ReportingEvent{
|
||||
Base: model.Base{CreatedAt: baseTime, UpdatedAt: baseTime},
|
||||
AccountID: s.testAccount.ID,
|
||||
Name: "first_response",
|
||||
Value: 10,
|
||||
ValueInBusinessHours: 5,
|
||||
ConversationID: &conversationID,
|
||||
EventStartTime: baseTime.Add(-time.Minute),
|
||||
EventEndTime: baseTime,
|
||||
}
|
||||
s.Require().NoError(s.db.Create(newer).Error)
|
||||
s.Require().NoError(s.db.Create(older).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/conversations/%d/reporting_events", s.accountURL(), s.testConv.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
|
||||
var resp []map[string]any
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Len(s.T(), resp, 2)
|
||||
assert.Equal(s.T(), float64(older.ID), resp[0]["id"])
|
||||
assert.Equal(s.T(), "first_response", resp[0]["name"])
|
||||
assert.Equal(s.T(), float64(10), resp[0]["value"])
|
||||
assert.Equal(s.T(), float64(5), resp[0]["value_in_business_hours"])
|
||||
assert.Equal(s.T(), float64(s.testAccount.ID), resp[0]["account_id"])
|
||||
assert.Equal(s.T(), float64(s.testConv.ID), resp[0]["conversation_id"])
|
||||
assert.Contains(s.T(), resp[0], "inbox_id")
|
||||
assert.Nil(s.T(), resp[0]["inbox_id"])
|
||||
assert.Contains(s.T(), resp[0], "user_id")
|
||||
assert.Nil(s.T(), resp[0]["user_id"])
|
||||
assert.Contains(s.T(), resp[0], "event_start_time")
|
||||
assert.Contains(s.T(), resp[0], "event_end_time")
|
||||
assert.NotContains(s.T(), resp[0], "success")
|
||||
assert.NotContains(s.T(), resp[0], "payload")
|
||||
assert.NotContains(s.T(), resp[0], "meta")
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestReportingEventsUsesDisplayIDAndScopesAccount() {
|
||||
displayID := uint(880)
|
||||
s.Require().NoError(s.db.Model(s.testConv).Update("display_id", displayID).Error)
|
||||
conversationID := s.testConv.ID
|
||||
event := &model.ReportingEvent{
|
||||
AccountID: s.testAccount.ID,
|
||||
Name: "reply_time",
|
||||
Value: 7,
|
||||
ValueInBusinessHours: 3,
|
||||
ConversationID: &conversationID,
|
||||
EventStartTime: time.Now().Add(-time.Minute).UTC(),
|
||||
EventEndTime: time.Now().UTC(),
|
||||
}
|
||||
s.Require().NoError(s.db.Create(event).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/conversations/%d/reporting_events", s.accountURL(), displayID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String())
|
||||
var resp []map[string]any
|
||||
assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Len(s.T(), resp, 1)
|
||||
assert.Equal(s.T(), float64(event.ID), resp[0]["id"])
|
||||
|
||||
otherAccount := &model.Account{Name: "OtherOrg", Locale: "en", Active: true}
|
||||
s.Require().NoError(s.db.Create(otherAccount).Error)
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/reporting_events", otherAccount.ID, displayID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestReportingEventsInvalidParams() {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/conversations/1/reporting_events", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/conversations/abc/reporting_events", s.testAccount.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestToggleTyping_InvalidAccountID() {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/conversations/1/toggle_typing", bytes.NewBufferString(`{"typing_status":"on"}`))
|
||||
|
||||
Reference in New Issue
Block a user