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.
146 lines
5.2 KiB
Go
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,
|
|
}
|
|
} |