Files
gochat/backend/internal/csat/service.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

146 lines
5.2 KiB
Go

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,
}
}