update
This commit is contained in:
@@ -123,18 +123,22 @@ func (h *CsatSurveyHandler) UpdateReviewNotes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, h.serializeCsatSurveyResponse(c.Request.Context(), resp))
|
||||
}
|
||||
|
||||
// Update updates a CSAT survey response (rating, feedback, review_notes).
|
||||
// Update updates CSAT review notes for an account-scoped survey response.
|
||||
// PATCH /api/v1/accounts/:account_id/csat_survey_responses/:id
|
||||
func (h *CsatSurveyHandler) Update(c *gin.Context) {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
id, err := parseUintParam(c, "id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
userID := getUserID(c)
|
||||
|
||||
var body struct {
|
||||
Rating int `json:"rating"`
|
||||
FeedbackMessage string `json:"feedback_message"`
|
||||
CsatReviewNotes string `json:"csat_review_notes"`
|
||||
ReviewNotes string `json:"review_notes"`
|
||||
}
|
||||
@@ -146,17 +150,32 @@ func (h *CsatSurveyHandler) Update(c *gin.Context) {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to update csat survey response")
|
||||
return
|
||||
}
|
||||
if _, err := h.svc.GetByIDForAccount(c.Request.Context(), accountID, id); err != nil {
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
notes := body.CsatReviewNotes
|
||||
if notes == "" {
|
||||
notes = body.ReviewNotes
|
||||
}
|
||||
resp, svcErr := h.svc.Update(c.Request.Context(), id, body.Rating, body.FeedbackMessage, notes)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
if err := h.svc.UpdateReviewNotes(c.Request.Context(), id, notes, userID); err != nil {
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
resp, err := h.svc.GetByIDForAccount(c.Request.Context(), accountID, id)
|
||||
if err != nil {
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
recordAuditMutation(c, h.auditSvc, auditMutation{
|
||||
AccountID: accountID,
|
||||
AuditableType: "CsatSurveyResponse",
|
||||
AuditableID: resp.ID,
|
||||
Action: "update",
|
||||
AuditedChanges: gin.H{"csat_review_notes": notes},
|
||||
})
|
||||
c.JSON(http.StatusOK, h.serializeCsatSurveyResponse(c.Request.Context(), resp))
|
||||
}
|
||||
|
||||
@@ -265,19 +284,13 @@ func buildCsatFilter(c *gin.Context) automation.CsatListFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// since filter (Chatwoot frontend sends Unix seconds; keep RFC3339 compatibility for local callers)
|
||||
if v := c.Query("since"); v != "" {
|
||||
t, err := parseCsatQueryTime(v)
|
||||
if err == nil {
|
||||
filter.Since = &t
|
||||
}
|
||||
}
|
||||
|
||||
// until filter (Chatwoot frontend sends Unix seconds; keep RFC3339 compatibility for local callers)
|
||||
if v := c.Query("until"); v != "" {
|
||||
t, err := parseCsatQueryTime(v)
|
||||
if err == nil {
|
||||
filter.Until = &t
|
||||
// Chatwoot DateRangeHelper applies a range only when both since and until are present.
|
||||
if sinceRaw, untilRaw := c.Query("since"), c.Query("until"); sinceRaw != "" && untilRaw != "" {
|
||||
since, sinceErr := parseCsatQueryTime(sinceRaw)
|
||||
until, untilErr := parseCsatQueryTime(untilRaw)
|
||||
if sinceErr == nil && untilErr == nil {
|
||||
filter.Since = &since
|
||||
filter.Until = &until
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,12 +300,6 @@ func buildCsatFilter(c *gin.Context) automation.CsatListFilter {
|
||||
filter.Page = int(n)
|
||||
}
|
||||
}
|
||||
if v := c.Query("per_page"); v != "" {
|
||||
if n, err := csatParseUintFull(v); err == nil && n > 0 {
|
||||
filter.PageSize = int(n)
|
||||
}
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
@@ -479,7 +486,7 @@ func (h *CsatSurveyHandler) Download(c *gin.Context) {
|
||||
contactEmail,
|
||||
contactPhone,
|
||||
conversationLink,
|
||||
r.CreatedAt.Format(time.RFC3339),
|
||||
formatCsatCSVTimestamp(r.CreatedAt),
|
||||
r.CsatReviewNotes,
|
||||
}
|
||||
if err := writer.Write(record); err != nil {
|
||||
@@ -502,6 +509,10 @@ func (h *CsatSurveyHandler) Download(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func formatCsatCSVTimestamp(value time.Time) string {
|
||||
return value.Format("2006-01-02 15:04:05 MST")
|
||||
}
|
||||
|
||||
func csatConversationURL(req *http.Request, accountID uint, conversation *model.Conversation) string {
|
||||
if conversation == nil {
|
||||
return ""
|
||||
|
||||
@@ -148,6 +148,62 @@ func (s *CsatSurveyHandlerTestSuite) TestList_ChatwootPayloadAndFilters() {
|
||||
assert.Equal(s.T(), reviewer.Name, reviewerPayload["name"])
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestList_IgnoresPerPageAndUsesChatwootFixedPageSize() {
|
||||
createdAt := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
|
||||
_, _, contact, conversation, _ := s.seedAccountCsatResponseGraph(createdAt, 5)
|
||||
for i := 0; i < 29; i++ {
|
||||
response := &automation.CsatSurveyResponse{
|
||||
AccountID: s.account.ID,
|
||||
ConversationID: conversation.ID,
|
||||
ContactID: contact.ID,
|
||||
Rating: 4,
|
||||
}
|
||||
s.Require().NoError(s.db.Create(response).Error)
|
||||
recordedAt := createdAt.Add(time.Duration(i+1) * time.Minute)
|
||||
s.Require().NoError(s.db.Model(response).Updates(map[string]any{"created_at": recordedAt, "updated_at": recordedAt}).Error)
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/api/v1/accounts/:account_id/csat_survey_responses", s.handler.List)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/csat_survey_responses?page=1&per_page=5", s.account.ID), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload []map[string]any
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
s.Require().Len(payload, 25)
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestList_UsesChatwootDateRangeBoundary() {
|
||||
baseTime := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
|
||||
_, _, contact, conversation, _ := s.seedAccountCsatResponseGraph(baseTime.Add(30*time.Minute), 5)
|
||||
onUntil := &automation.CsatSurveyResponse{AccountID: s.account.ID, ConversationID: conversation.ID, ContactID: contact.ID, Rating: 4}
|
||||
s.Require().NoError(s.db.Create(onUntil).Error)
|
||||
s.Require().NoError(s.db.Model(onUntil).Updates(map[string]any{"created_at": baseTime.Add(time.Hour), "updated_at": baseTime.Add(time.Hour)}).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/api/v1/accounts/:account_id/csat_survey_responses", s.handler.List)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/csat_survey_responses?since=%d", s.account.ID, baseTime.Add(30*time.Minute).Unix()), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload []map[string]any
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
s.Require().Len(payload, 2)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/csat_survey_responses?since=%d&until=%d", s.account.ID, baseTime.Unix(), baseTime.Add(time.Hour).Unix()), nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
payload = nil
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
s.Require().Len(payload, 1)
|
||||
assert.Equal(s.T(), float64(5), payload[0]["rating"])
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestMetrics_ChatwootPayloadAndFilters() {
|
||||
createdAt := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
|
||||
agent, _, _, conversation, _ := s.seedAccountCsatResponseGraph(createdAt, 5)
|
||||
@@ -206,11 +262,16 @@ func (s *CsatSurveyHandlerTestSuite) TestDownload_ChatwootCSVAndFilters() {
|
||||
assert.Equal(s.T(), contact.Email, rows[1][4])
|
||||
assert.Equal(s.T(), contact.PhoneNumber, rows[1][5])
|
||||
assert.Equal(s.T(), fmt.Sprintf("https://app.example.test/app/accounts/%d/conversations/42", s.account.ID), rows[1][6])
|
||||
assert.Equal(s.T(), createdAt.Format(time.RFC3339), rows[1][7])
|
||||
assert.Equal(s.T(), "2026-06-05 10:30:00 UTC", rows[1][7])
|
||||
assert.Equal(s.T(), "Needs follow up", rows[1][8])
|
||||
assert.Equal(s.T(), "Reporting period 1970-01-01 to 3000-01-01", rows[2][0])
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestDownload_FormatsRecordedAtLikeChatwootCSV() {
|
||||
recordedAt := time.Date(2026, 6, 5, 10, 30, 0, 0, time.UTC)
|
||||
assert.Equal(s.T(), "2026-06-05 10:30:00 UTC", formatCsatCSVTimestamp(recordedAt))
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestUpdateReviewNotes_Success() {
|
||||
_, reviewer, _, _, _ := s.seedAccountCsatResponseGraph(time.Now().Add(-time.Hour), 5)
|
||||
var survey automation.CsatSurveyResponse
|
||||
@@ -237,22 +298,49 @@ func (s *CsatSurveyHandlerTestSuite) TestUpdateReviewNotes_Success() {
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestUpdate_Success() {
|
||||
survey := &automation.CsatSurveyResponse{AccountID: s.account.ID, ConversationID: 1, Rating: 5}
|
||||
_, reviewer, _, _, _ := s.seedAccountCsatResponseGraph(time.Now().Add(-time.Hour), 5)
|
||||
var survey automation.CsatSurveyResponse
|
||||
s.Require().NoError(s.db.First(&survey).Error)
|
||||
originalRating := survey.Rating
|
||||
|
||||
r := gin.New()
|
||||
r.PATCH("/api/v1/accounts/:account_id/csat_survey_responses/:id", func(c *gin.Context) {
|
||||
c.Set("user_id", float64(reviewer.ID))
|
||||
s.handler.Update(c)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"csat_review_notes":"updated notes","rating":1,"feedback_message":"ignored"}`
|
||||
req, _ := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/accounts/%d/csat_survey_responses/%d", s.account.ID, survey.ID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
var payload map[string]any
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
assert.Equal(s.T(), "updated notes", payload["csat_review_notes"])
|
||||
assert.Equal(s.T(), float64(originalRating), payload["rating"])
|
||||
reviewerPayload := payload["review_notes_updated_by"].(map[string]any)
|
||||
assert.Equal(s.T(), reviewer.Name, reviewerPayload["name"])
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestUpdate_NotFoundAcrossAccountScope() {
|
||||
survey := &automation.CsatSurveyResponse{AccountID: s.account.ID + 1, ConversationID: 1, ContactID: 1, Rating: 5}
|
||||
s.Require().NoError(s.db.Create(survey).Error)
|
||||
|
||||
r := gin.New()
|
||||
r.PUT("/api/v1/accounts/:account_id/csats/:id", func(c *gin.Context) {
|
||||
r.PATCH("/api/v1/accounts/:account_id/csat_survey_responses/:id", func(c *gin.Context) {
|
||||
c.Set("user_id", float64(1))
|
||||
s.handler.Update(c)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"review_notes":"updated notes"}`
|
||||
req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/csats/%d", s.account.ID, survey.ID), bytes.NewBufferString(body))
|
||||
body := `{"csat_review_notes":"updated notes"}`
|
||||
req, _ := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/accounts/%d/csat_survey_responses/%d", s.account.ID, survey.ID), bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestPublicCsatShowAndUpdate_Success() {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
)
|
||||
@@ -33,7 +34,7 @@ func TestInboxHandler_ChatwootSerializerParity(t *testing.T) {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}))
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}, &channelmodel.ChannelAPI{}))
|
||||
|
||||
account := &model.Account{Name: "Inbox Parity", Locale: "en", Active: true}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
@@ -123,7 +124,7 @@ func TestInboxHandler_ChatwootCreateUpdateRequestBinding(t *testing.T) {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}))
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}, &channelmodel.ChannelAPI{}))
|
||||
|
||||
account := &model.Account{Name: "Inbox Binding", Locale: "en", Active: true}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
@@ -256,7 +257,7 @@ func TestInboxHandler_ChatwootCreateRejectsAccountInboxLimit(t *testing.T) {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}))
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}, &channelmodel.ChannelAPI{}))
|
||||
|
||||
account := &model.Account{Name: "Inbox Limit", Locale: "en", Active: true, InboxLimit: 1}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
@@ -290,7 +291,7 @@ func TestInboxHandler_ChatwootChannelSpecificConfigDepth(t *testing.T) {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}))
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}, &channelmodel.ChannelAPI{}))
|
||||
|
||||
account := &model.Account{Name: "Inbox Channel Depth", Locale: "en", Active: true}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
|
||||
Reference in New Issue
Block a user