package csat import ( "context" "time" "gorm.io/gorm" ) // CsatSurveyService provides business logic for CSAT survey operations. // Reference: Chatwoot CsatSurveyService — CRUD for survey responses. type CsatSurveyService struct { db *gorm.DB } func NewCsatSurveyService(db *gorm.DB) *CsatSurveyService { return &CsatSurveyService{db: db} } // Create records a new CSAT survey response. func (s *CsatSurveyService) Create(ctx context.Context, response *CsatSurveyResponse) error { return s.db.WithContext(ctx).Create(response).Error } // GetByID retrieves a CSAT survey response by ID. func (s *CsatSurveyService) GetByID(ctx context.Context, id uint) (*CsatSurveyResponse, error) { var resp CsatSurveyResponse if err := s.db.WithContext(ctx).First(&resp, id).Error; err != nil { return nil, err } return &resp, nil } // ListByAccount returns CSAT responses for an account with Chatwoot-compatible filters. // 1:1 Chatwoot: CsatSurveyResponsesController#index // Filters: created_at range, assigned_agent_id (user_ids), inbox_id, team_id, rating // Pagination: 25 per page func (s *CsatSurveyService) ListByAccount(ctx context.Context, accountID uint, params CsatFilterParams, offset, limit int) ([]CsatSurveyResponse, int64, error) { var responses []CsatSurveyResponse var count int64 db := s.db.WithContext(ctx).Model(&CsatSurveyResponse{}).Where("account_id = ?", accountID) // 1:1 Chatwoot: filter_by_created_at(range) if params.Since != nil { db = db.Where("created_at >= ?", params.Since) } if params.Until != nil { db = db.Where("created_at <= ?", params.Until) } // 1:1 Chatwoot: filter_by_assigned_agent_id(user_ids) if len(params.AssignedAgentIDs) > 0 { db = db.Where("assigned_agent_id IN ?", params.AssignedAgentIDs) } // 1:1 Chatwoot: filter_by_inbox_id // CSAT responses don't have inbox_id directly; join through conversations if params.InboxID > 0 { db = db.Joins("JOIN conversations ON conversations.id = csat_survey_responses.conversation_id"). Where("conversations.inbox_id = ?", params.InboxID) } // 1:1 Chatwoot: filter_by_team_id // Team filtering through conversation team_id if params.TeamID > 0 { db = db.Joins("JOIN conversations ON conversations.id = csat_survey_responses.conversation_id"). Where("conversations.team_id = ?", params.TeamID) } // 1:1 Chatwoot: filter_by_rating if params.Rating > 0 { db = db.Where("rating = ?", params.Rating) } db.Count(&count) if err := db.Offset(offset).Limit(limit).Order("created_at DESC").Find(&responses).Error; err != nil { return nil, 0, err } return responses, count, nil } // CsatFilterParams holds filter parameters for CSAT survey listing. // 1:1 Chatwoot: CsatSurveyResponsesController filter params type CsatFilterParams struct { Since *time.Time // created_at >= since Until *time.Time // created_at <= until AssignedAgentIDs []uint // user_ids param InboxID uint // inbox_id param TeamID uint // team_id param Rating int // rating param (1-5) } // ListByConversation returns CSAT responses for a specific conversation. func (s *CsatSurveyService) ListByConversation(ctx context.Context, accountID, conversationID uint) ([]CsatSurveyResponse, error) { var responses []CsatSurveyResponse err := s.db.WithContext(ctx). Where("account_id = ? AND conversation_id = ?", accountID, conversationID). Order("created_at DESC").Find(&responses).Error return responses, err } // UpdateReviewNotes updates the review notes on a CSAT response. func (s *CsatSurveyService) UpdateReviewNotes(ctx context.Context, id uint, notes string, updatedByUserID uint) error { return s.db.WithContext(ctx).Model(&CsatSurveyResponse{}). Where("id = ?", id). Updates(map[string]interface{}{ "csat_review_notes": notes, "review_notes_updated_by_id": updatedByUserID, }).Error } // GetAverageRating computes the average CSAT rating for an account. func (s *CsatSurveyService) GetAverageRating(ctx context.Context, accountID uint) (float64, int64, error) { var result struct { Avg float64 Count int64 } err := s.db.WithContext(ctx).Model(&CsatSurveyResponse{}). Where("account_id = ?", accountID). Select("AVG(rating) as avg, COUNT(*) as count"). Scan(&result).Error return result.Avg, result.Count, err } // ResponseBuilder constructs a CsatSurveyResponse from event data. // Reference: Chatwoot CsatSurveyListener — on message_updated with input_csat content type, // builds response from the message survey data. type ResponseBuilder struct{} func NewResponseBuilder() *ResponseBuilder { return &ResponseBuilder{} } // BuildFromSurveyInput creates a CsatSurveyResponse from CSAT survey input data. // The input_csat content type message carries rating and feedback in its content/metadata. func (b *ResponseBuilder) BuildFromSurveyInput(accountID, conversationID, contactID, messageID uint, assignedAgentID *uint, rating int, feedback string) *CsatSurveyResponse { return &CsatSurveyResponse{ AccountID: accountID, ConversationID: conversationID, ContactID: contactID, MessageID: messageID, AssignedAgentID: assignedAgentID, Rating: rating, FeedbackMessage: feedback, } }