feat(csat): send surveys on resolved conversations
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gochat/gochat/internal/channel"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CsatSurveyListener sends CSAT surveys when conversations are resolved.
|
||||
@@ -25,62 +27,76 @@ func (l *CsatSurveyListener) Name() string {
|
||||
|
||||
// OnEvent processes incoming channel events.
|
||||
func (l *CsatSurveyListener) OnEvent(ctx context.Context, event *channel.ChannelEvent) error {
|
||||
if event.Type == channel.EventMessageUpdated {
|
||||
return l.onMessageUpdated(ctx, event)
|
||||
}
|
||||
if event.Type != channel.EventConversationResolved {
|
||||
return nil
|
||||
}
|
||||
|
||||
accountID, ok := extractUint(event.Data, "account_id")
|
||||
if !ok {
|
||||
applogger.L().Warnf("csat_survey_listener: missing account_id in event %s", event.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
conversationID, ok := extractUint(event.Data, "conversation_id")
|
||||
conversationID, ok := eventUint(event, "conversation_id")
|
||||
if !ok {
|
||||
applogger.L().Warnf("csat_survey_listener: missing conversation_id in event %s", event.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Check if CSAT is enabled for the inbox associated with this conversation.
|
||||
// The Inbox model currently lacks csat_enabled/csat_message fields.
|
||||
// When those fields are added, load the conversation's inbox and check inbox.CSATEnabled.
|
||||
|
||||
// Check if a CSAT survey was already sent for this conversation
|
||||
var count int64
|
||||
l.db.DB().WithContext(ctx).
|
||||
Model(&CsatSurveyResponse{}).
|
||||
Where("conversation_id = ?", conversationID).
|
||||
Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
applogger.L().Debugf("csat_survey_listener: CSAT already sent for conversation %d, skipping", conversationID)
|
||||
return nil
|
||||
}
|
||||
|
||||
applogger.L().Infof("csat_survey_listener: creating CSAT survey record for account %d conversation %d", accountID, conversationID)
|
||||
|
||||
// Create a pending CSAT survey response record
|
||||
// The customer will submit their rating via the public endpoint
|
||||
survey := &CsatSurveyResponse{
|
||||
AccountID: accountID,
|
||||
ConversationID: conversationID,
|
||||
Rating: 0, // pending — customer hasn't submitted yet
|
||||
}
|
||||
|
||||
if err := l.db.DB().WithContext(ctx).Create(survey).Error; err != nil {
|
||||
applogger.L().Errorf("csat_survey_listener: failed to create CSAT survey for conversation %d: %v", conversationID, err)
|
||||
message, err := NewCsatSurveyService(l.db).SendSurveyForConversationID(ctx, conversationID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("csat_survey_listener: failed to send CSAT survey for conversation %d: %v", conversationID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
applogger.L().Infof("csat_survey_listener: CSAT survey record %d created for conversation %d", survey.ID, conversationID)
|
||||
|
||||
// TODO: Send CSAT message to the contact via the channel (ActionService.SendCSATSurvey pattern)
|
||||
// This requires loading the conversation's inbox, contact, and channel to dispatch the message.
|
||||
// Will be wired when the messaging pipeline is fully integrated.
|
||||
if message != nil {
|
||||
applogger.L().Infof("csat_survey_listener: CSAT survey message %d ready for conversation %d", message.ID, conversationID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *CsatSurveyListener) onMessageUpdated(ctx context.Context, event *channel.ChannelEvent) error {
|
||||
messageID, ok := eventUint(event, "message_id")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var message model.Message
|
||||
if err := l.db.DB().WithContext(ctx).First(&message, messageID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if message.ContentType != "input_csat" {
|
||||
return nil
|
||||
}
|
||||
var conversation model.Conversation
|
||||
if err := l.db.DB().WithContext(ctx).First(&conversation, message.ConversationID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
submittedValues := jsonMap(message.ContentAttributes)["submitted_values"]
|
||||
if _, _, ok := ExtractCsatSubmittedValues(submittedValues); !ok {
|
||||
return nil
|
||||
}
|
||||
_, err := ApplyCsatSubmission(ctx, l.db.DB(), &message, &conversation, submittedValues)
|
||||
return err
|
||||
}
|
||||
|
||||
func eventUint(event *channel.ChannelEvent, key string) (uint, bool) {
|
||||
switch key {
|
||||
case "account_id":
|
||||
if event.AccountID != 0 {
|
||||
return event.AccountID, true
|
||||
}
|
||||
case "conversation_id":
|
||||
if event.ConversationID != 0 {
|
||||
return event.ConversationID, true
|
||||
}
|
||||
case "message_id":
|
||||
if value, ok := extractUint(event.Data, key); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return extractUint(event.Data, key)
|
||||
}
|
||||
|
||||
// extractUint extracts a uint value from a map[string]interface{}.
|
||||
func extractUint(data map[string]interface{}, key string) (uint, bool) {
|
||||
v, ok := data[key]
|
||||
@@ -100,4 +116,4 @@ func extractUint(data map[string]interface{}, key string) (uint, bool) {
|
||||
return uint(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/gochat/gochat/internal/channel"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestCsatSurveyListener_ResolvedConversationSendsOneSurveyMessage(t *testing.T) {
|
||||
dbProvider := setupAutomationTestDBProvider(t)
|
||||
db := dbProvider.DB()
|
||||
accountID, _ := seedTestAccount(db, t)
|
||||
conversation := seedCsatListenerConversation(t, db, accountID, true)
|
||||
listener := NewCsatSurveyListener(dbProvider)
|
||||
event := &channel.ChannelEvent{Type: channel.EventConversationResolved, ConversationID: conversation.ID}
|
||||
|
||||
if err := listener.OnEvent(context.Background(), event); err != nil {
|
||||
t.Fatalf("expected no listener error, got %v", err)
|
||||
}
|
||||
if err := listener.OnEvent(context.Background(), event); err != nil {
|
||||
t.Fatalf("expected idempotent listener call, got %v", err)
|
||||
}
|
||||
|
||||
var messages []model.Message
|
||||
if err := db.Where("conversation_id = ? AND content_type = ?", conversation.ID, "input_csat").Find(&messages).Error; err != nil {
|
||||
t.Fatalf("failed to load csat messages: %v", err)
|
||||
}
|
||||
if len(messages) != 1 {
|
||||
t.Fatalf("expected exactly one CSAT message, got %d", len(messages))
|
||||
}
|
||||
if messages[0].MessageType != "template" {
|
||||
t.Fatalf("expected template message type, got %q", messages[0].MessageType)
|
||||
}
|
||||
if messages[0].Content != "Rate this chat" {
|
||||
t.Fatalf("expected inbox CSAT message content, got %q", messages[0].Content)
|
||||
}
|
||||
var attrs map[string]any
|
||||
if err := json.Unmarshal(messages[0].ContentAttributes, &attrs); err != nil {
|
||||
t.Fatalf("failed to decode content attributes: %v", err)
|
||||
}
|
||||
if attrs["display_type"] != "star" {
|
||||
t.Fatalf("expected star display_type, got %v", attrs["display_type"])
|
||||
}
|
||||
|
||||
var responseCount int64
|
||||
if err := db.Model(&CsatSurveyResponse{}).Where("conversation_id = ?", conversation.ID).Count(&responseCount).Error; err != nil {
|
||||
t.Fatalf("failed to count responses: %v", err)
|
||||
}
|
||||
if responseCount != 0 {
|
||||
t.Fatalf("expected no response before customer submits rating, got %d", responseCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCsatSurveyListener_ResolvedConversationSkipsDisabledInbox(t *testing.T) {
|
||||
dbProvider := setupAutomationTestDBProvider(t)
|
||||
db := dbProvider.DB()
|
||||
accountID, _ := seedTestAccount(db, t)
|
||||
conversation := seedCsatListenerConversation(t, db, accountID, false)
|
||||
listener := NewCsatSurveyListener(dbProvider)
|
||||
event := &channel.ChannelEvent{Type: channel.EventConversationResolved, ConversationID: conversation.ID}
|
||||
|
||||
if err := listener.OnEvent(context.Background(), event); err != nil {
|
||||
t.Fatalf("expected no listener error, got %v", err)
|
||||
}
|
||||
|
||||
var messageCount int64
|
||||
if err := db.Model(&model.Message{}).Where("conversation_id = ? AND content_type = ?", conversation.ID, "input_csat").Count(&messageCount).Error; err != nil {
|
||||
t.Fatalf("failed to count messages: %v", err)
|
||||
}
|
||||
if messageCount != 0 {
|
||||
t.Fatalf("expected no CSAT messages for disabled inbox, got %d", messageCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCsatSurveyListener_MessageUpdatedBuildsResponse(t *testing.T) {
|
||||
dbProvider := setupAutomationTestDBProvider(t)
|
||||
db := dbProvider.DB()
|
||||
accountID, userID := seedTestAccount(db, t)
|
||||
conversation := seedCsatListenerConversation(t, db, accountID, true)
|
||||
conversation.AssigneeID = &userID
|
||||
if err := db.Save(conversation).Error; err != nil {
|
||||
t.Fatalf("failed to assign conversation: %v", err)
|
||||
}
|
||||
message := &model.Message{
|
||||
ConversationID: conversation.ID,
|
||||
AccountID: conversation.AccountID,
|
||||
InboxID: conversation.InboxID,
|
||||
ContentType: "input_csat",
|
||||
MessageType: "template",
|
||||
Status: "sent",
|
||||
ContentAttributes: datatypes.JSON(
|
||||
`{"submitted_values":{"csat_survey_response":{"rating":5,"feedback_message":"Great"}}}`,
|
||||
),
|
||||
}
|
||||
if err := db.Create(message).Error; err != nil {
|
||||
t.Fatalf("failed to create message: %v", err)
|
||||
}
|
||||
listener := NewCsatSurveyListener(dbProvider)
|
||||
event := &channel.ChannelEvent{Type: channel.EventMessageUpdated, Data: map[string]any{"message_id": message.ID}}
|
||||
|
||||
if err := listener.OnEvent(context.Background(), event); err != nil {
|
||||
t.Fatalf("expected no listener error, got %v", err)
|
||||
}
|
||||
|
||||
var response CsatSurveyResponse
|
||||
if err := db.Where("message_id = ?", message.ID).First(&response).Error; err != nil {
|
||||
t.Fatalf("expected CSAT response, got %v", err)
|
||||
}
|
||||
if response.Rating != 5 || response.FeedbackMessage != "Great" {
|
||||
t.Fatalf("unexpected response payload: rating=%d feedback=%q", response.Rating, response.FeedbackMessage)
|
||||
}
|
||||
if response.AssignedAgentID == nil || *response.AssignedAgentID != userID {
|
||||
t.Fatalf("expected assigned agent %d, got %v", userID, response.AssignedAgentID)
|
||||
}
|
||||
}
|
||||
|
||||
func seedCsatListenerConversation(t *testing.T, db *gorm.DB, accountID uint, csatEnabled bool) *model.Conversation {
|
||||
t.Helper()
|
||||
contact := &model.Contact{AccountID: accountID, Name: "CSAT Contact"}
|
||||
if err := db.Create(contact).Error; err != nil {
|
||||
t.Fatalf("failed to create contact: %v", err)
|
||||
}
|
||||
inbox := &model.Inbox{
|
||||
AccountID: accountID,
|
||||
Name: "CSAT Inbox",
|
||||
ChannelType: "web_widget",
|
||||
Enabled: true,
|
||||
CsatSurveyEnabled: csatEnabled,
|
||||
CsatConfig: `{"display_type":"star","message":"Rate this chat"}`,
|
||||
AllowMessagesAfterResolved: true,
|
||||
}
|
||||
if err := db.Create(inbox).Error; err != nil {
|
||||
t.Fatalf("failed to create inbox: %v", err)
|
||||
}
|
||||
conversation := &model.Conversation{
|
||||
AccountID: accountID,
|
||||
InboxID: inbox.ID,
|
||||
ContactID: contact.ID,
|
||||
Status: "resolved",
|
||||
ChannelType: inbox.ChannelType,
|
||||
Channel: inbox.ChannelType,
|
||||
}
|
||||
if err := db.Create(conversation).Error; err != nil {
|
||||
t.Fatalf("failed to create conversation: %v", err)
|
||||
}
|
||||
return conversation
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
@@ -106,7 +107,7 @@ func (s *CsatSurveyService) GetPublicSurveyByConversationUUID(ctx context.Contex
|
||||
return s.publicCsatPayload(ctx, message, conversation, inbox, account)
|
||||
}
|
||||
|
||||
func (s *CsatSurveyService) SubmitPublicSurveyByConversationUUID(ctx context.Context, conversationUUID string, submittedValues []map[string]any) (*PublicCsatSurvey, error) {
|
||||
func (s *CsatSurveyService) SubmitPublicSurveyByConversationUUID(ctx context.Context, conversationUUID string, submittedValues any) (*PublicCsatSurvey, error) {
|
||||
message, conversation, inbox, account, err := s.findPublicCsatMessage(ctx, conversationUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,6 +121,67 @@ func (s *CsatSurveyService) SubmitPublicSurveyByConversationUUID(ctx context.Con
|
||||
return s.publicCsatPayload(ctx, message, conversation, inbox, account)
|
||||
}
|
||||
|
||||
func (s *CsatSurveyService) SendSurveyForConversationID(ctx context.Context, conversationID uint) (*model.Message, error) {
|
||||
var conversation model.Conversation
|
||||
if err := s.db.DB().WithContext(ctx).First(&conversation, conversationID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.SendSurveyForConversation(ctx, &conversation)
|
||||
}
|
||||
|
||||
func (s *CsatSurveyService) SendSurveyForConversation(ctx context.Context, conversation *model.Conversation) (*model.Message, error) {
|
||||
if conversation == nil {
|
||||
return nil, errors.New("conversation is required")
|
||||
}
|
||||
if conversation.Status != "resolved" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var inbox model.Inbox
|
||||
if err := s.db.DB().WithContext(ctx).First(&inbox, conversation.InboxID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !inbox.CsatSurveyEnabled || !csatAllowedBySurveyRules(inbox.CsatConfig, conversation.Labels) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var existing model.Message
|
||||
err := s.db.DB().WithContext(ctx).
|
||||
Where("conversation_id = ? AND content_type = ?", conversation.ID, "input_csat").
|
||||
Order("id ASC").First(&existing).Error
|
||||
if err == nil {
|
||||
return &existing, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := csatConfigMap(inbox.CsatConfig)
|
||||
content, _ := config["message"].(string)
|
||||
if content == "" {
|
||||
content = "Please rate this conversation"
|
||||
}
|
||||
displayType, _ := config["display_type"].(string)
|
||||
if displayType == "" {
|
||||
displayType = "emoji"
|
||||
}
|
||||
attrs := mustJSON(map[string]any{"display_type": displayType})
|
||||
message := &model.Message{
|
||||
ConversationID: conversation.ID,
|
||||
AccountID: conversation.AccountID,
|
||||
InboxID: conversation.InboxID,
|
||||
Content: content,
|
||||
ContentType: "input_csat",
|
||||
MessageType: "template",
|
||||
Status: "sent",
|
||||
ContentAttributes: attrs,
|
||||
}
|
||||
if err := s.db.DB().WithContext(ctx).Create(message).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// ListByAccount retrieves CSAT survey responses for an account with optional filters.
|
||||
func (s *CsatSurveyService) ListByAccount(ctx context.Context, accountID uint, filter CsatListFilter) ([]CsatSurveyResponse, int, error) {
|
||||
var responses []CsatSurveyResponse
|
||||
@@ -189,7 +251,7 @@ func (s *CsatSurveyService) UpdateResponse(ctx context.Context, id uint, rating
|
||||
|
||||
var ErrCsatSurveyLocked = errors.New("You cannot update the CSAT survey after 14 days")
|
||||
|
||||
func ApplyCsatSubmission(ctx context.Context, db *gorm.DB, message *model.Message, conversation *model.Conversation, submittedValues []map[string]any) (*CsatSurveyResponse, error) {
|
||||
func ApplyCsatSubmission(ctx context.Context, db *gorm.DB, message *model.Message, conversation *model.Conversation, submittedValues any) (*CsatSurveyResponse, error) {
|
||||
if message.ContentType != "input_csat" {
|
||||
return nil, errors.New("invalid CSAT survey message")
|
||||
}
|
||||
@@ -231,8 +293,8 @@ func ApplyCsatSubmission(ctx context.Context, db *gorm.DB, message *model.Messag
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func ExtractCsatSubmittedValues(submittedValues []map[string]any) (int, string, bool) {
|
||||
for _, value := range submittedValues {
|
||||
func ExtractCsatSubmittedValues(submittedValues any) (int, string, bool) {
|
||||
for _, value := range submittedValueMaps(submittedValues) {
|
||||
if raw, ok := value["csat_survey_response"]; ok {
|
||||
if rating, feedback, ok := extractCsatResponse(raw); ok {
|
||||
return rating, feedback, true
|
||||
@@ -246,6 +308,27 @@ func ExtractCsatSubmittedValues(submittedValues []map[string]any) (int, string,
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
func submittedValueMaps(submittedValues any) []map[string]any {
|
||||
switch values := submittedValues.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case map[string]any:
|
||||
return []map[string]any{values}
|
||||
case []map[string]any:
|
||||
return values
|
||||
case []any:
|
||||
out := make([]map[string]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
if mapped, ok := value.(map[string]any); ok {
|
||||
out = append(out, mapped)
|
||||
}
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func IsCsatSurveyLocked(createdAt, now time.Time) bool {
|
||||
createdDate := dateOnly(createdAt)
|
||||
nowDate := dateOnly(now.In(createdAt.Location()))
|
||||
@@ -482,6 +565,73 @@ func csatConfigMap(raw string) map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
func csatAllowedBySurveyRules(rawConfig, rawLabels string) bool {
|
||||
config := csatConfigMap(rawConfig)
|
||||
rawRules, ok := config["survey_rules"].(map[string]any)
|
||||
if !ok || len(rawRules) == 0 {
|
||||
return true
|
||||
}
|
||||
ruleValues := stringList(rawRules["values"])
|
||||
if len(ruleValues) == 0 {
|
||||
return true
|
||||
}
|
||||
labels := labelSet(rawLabels)
|
||||
operator, _ := rawRules["operator"].(string)
|
||||
if operator == "" {
|
||||
operator = "contains"
|
||||
}
|
||||
for _, value := range ruleValues {
|
||||
_, exists := labels[value]
|
||||
if operator == "does_not_contain" && exists {
|
||||
return false
|
||||
}
|
||||
if operator != "does_not_contain" && exists {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return operator == "does_not_contain"
|
||||
}
|
||||
|
||||
func stringList(raw any) []string {
|
||||
switch values := raw.(type) {
|
||||
case []string:
|
||||
return values
|
||||
case []any:
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if s, ok := value.(string); ok && s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func labelSet(raw string) map[string]struct{} {
|
||||
labels := map[string]struct{}{}
|
||||
if raw == "" {
|
||||
return labels
|
||||
}
|
||||
var jsonLabels []string
|
||||
if err := json.Unmarshal([]byte(raw), &jsonLabels); err == nil {
|
||||
for _, label := range jsonLabels {
|
||||
if label != "" {
|
||||
labels[label] = struct{}{}
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
for _, label := range strings.Split(raw, ",") {
|
||||
label = strings.TrimSpace(label)
|
||||
if label != "" {
|
||||
labels[label] = struct{}{}
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func dateOnly(value time.Time) time.Time {
|
||||
year, month, day := value.Date()
|
||||
return time.Date(year, month, day, 0, 0, 0, 0, value.Location())
|
||||
|
||||
@@ -201,12 +201,12 @@ type publicCsatUpdateBody struct {
|
||||
Rating int `json:"rating"`
|
||||
FeedbackMessage string `json:"feedback_message"`
|
||||
Message struct {
|
||||
SubmittedValues []map[string]any `json:"submitted_values"`
|
||||
SubmittedValues any `json:"submitted_values"`
|
||||
} `json:"message"`
|
||||
}
|
||||
|
||||
func (b publicCsatUpdateBody) submittedValues() []map[string]any {
|
||||
if len(b.Message.SubmittedValues) > 0 {
|
||||
func (b publicCsatUpdateBody) submittedValues() any {
|
||||
if b.Message.SubmittedValues != nil {
|
||||
return b.Message.SubmittedValues
|
||||
}
|
||||
if b.Rating != 0 || b.FeedbackMessage != "" {
|
||||
|
||||
@@ -232,7 +232,7 @@ func (s *CsatSurveyHandlerTestSuite) TestPublicCsatShowAndUpdate_Success() {
|
||||
assert.Equal(s.T(), "Rate this chat", showResp["content"])
|
||||
assert.Equal(s.T(), "CSAT Inbox", showResp["inbox_name"])
|
||||
|
||||
body := `{"message":{"submitted_values":[{"csat_survey_response":{"rating":4,"feedback_message":"Helpful"}}]}}`
|
||||
body := `{"message":{"submitted_values":{"csat_survey_response":{"rating":4,"feedback_message":"Helpful"}}}}`
|
||||
wUpdate := httptest.NewRecorder()
|
||||
reqUpdate, _ := http.NewRequest("PATCH", "/public/api/v1/csat_survey/"+conversation.UUID, bytes.NewBufferString(body))
|
||||
reqUpdate.Header.Set("Content-Type", "application/json")
|
||||
@@ -257,7 +257,22 @@ func (s *CsatSurveyHandlerTestSuite) TestPublicCsatShowAndUpdate_Success() {
|
||||
s.Require().NoError(s.db.First(&storedMessage, message.ID).Error)
|
||||
var attrs map[string]any
|
||||
s.Require().NoError(json.Unmarshal(storedMessage.ContentAttributes, &attrs))
|
||||
assert.NotEmpty(s.T(), attrs["submitted_values"])
|
||||
submittedValues := attrs["submitted_values"].(map[string]any)
|
||||
assert.Contains(s.T(), submittedValues, "csat_survey_response")
|
||||
|
||||
secondBody := `{"message":{"submitted_values":{"csat_survey_response":{"rating":2,"feedback_message":"Could be better"}}}}`
|
||||
wSecondUpdate := httptest.NewRecorder()
|
||||
reqSecondUpdate, _ := http.NewRequest("PATCH", "/public/api/v1/csat_survey/"+conversation.UUID, bytes.NewBufferString(secondBody))
|
||||
reqSecondUpdate.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(wSecondUpdate, reqSecondUpdate)
|
||||
assert.Equal(s.T(), http.StatusOK, wSecondUpdate.Code)
|
||||
|
||||
var count int64
|
||||
s.Require().NoError(s.db.Model(&automation.CsatSurveyResponse{}).Where("message_id = ?", message.ID).Count(&count).Error)
|
||||
assert.Equal(s.T(), int64(1), count)
|
||||
s.Require().NoError(s.db.Where("message_id = ?", message.ID).First(&stored).Error)
|
||||
assert.Equal(s.T(), 2, stored.Rating)
|
||||
assert.Equal(s.T(), "Could be better", stored.FeedbackMessage)
|
||||
}
|
||||
|
||||
func (s *CsatSurveyHandlerTestSuite) TestPublicCsatUpdate_LockedAfter14Days() {
|
||||
|
||||
Reference in New Issue
Block a user