339 lines
9.6 KiB
Go
339 lines
9.6 KiB
Go
package v1
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gochat/gochat/internal/automation"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// CsatSurveyHandler handles CSAT survey API endpoints.
|
|
// Reference: Chatwoot CsatSurveyResponsesController — list, metrics, review notes, public access
|
|
type CsatSurveyHandler struct {
|
|
svc *automation.CsatSurveyService
|
|
}
|
|
|
|
// NewCsatSurveyHandler creates a new CsatSurveyHandler.
|
|
func NewCsatSurveyHandler(svc *automation.CsatSurveyService) *CsatSurveyHandler {
|
|
return &CsatSurveyHandler{svc: svc}
|
|
}
|
|
|
|
// List retrieves CSAT survey responses for an account with optional filters.
|
|
// GET /api/v1/accounts/:account_id/csat_survey_responses
|
|
func (h *CsatSurveyHandler) List(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
filter := buildCsatFilter(c)
|
|
|
|
responses, total, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, filter)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"csat_survey_responses": responses,
|
|
"meta": gin.H{
|
|
"count": total,
|
|
"page": filter.Page,
|
|
"per_page": filter.PageSize,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Metrics computes aggregated CSAT statistics for an account.
|
|
// GET /api/v1/accounts/:account_id/csat_survey_responses/metrics
|
|
func (h *CsatSurveyHandler) Metrics(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
filter := buildCsatFilter(c)
|
|
|
|
metrics, svcErr := h.svc.Metrics(c.Request.Context(), accountID, filter)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
response.OK(c, metrics)
|
|
}
|
|
|
|
// UpdateReviewNotes updates the review notes on a CSAT survey response.
|
|
// POST /api/v1/accounts/:account_id/csat_survey_responses/:id/update_review_notes
|
|
func (h *CsatSurveyHandler) UpdateReviewNotes(c *gin.Context) {
|
|
id, err := parseUintParam(c, "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
userID := getUserID(c)
|
|
|
|
var body struct {
|
|
CsatReviewNotes string `json:"csat_review_notes"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.UpdateReviewNotes(c.Request.Context(), id, body.CsatReviewNotes, userID); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"id": id,
|
|
"csat_review_notes": body.CsatReviewNotes,
|
|
"updated_by_id": userID,
|
|
})
|
|
}
|
|
|
|
// Update updates a CSAT survey response (rating, feedback, review_notes).
|
|
// PATCH /api/v1/accounts/:account_id/csat_survey_responses/:id
|
|
func (h *CsatSurveyHandler) Update(c *gin.Context) {
|
|
id, err := parseUintParam(c, "id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Rating int `json:"rating"`
|
|
FeedbackMessage string `json:"feedback_message"`
|
|
CsatReviewNotes string `json:"csat_review_notes"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
if !h.svc.Ready() {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to update csat survey response")
|
|
return
|
|
}
|
|
|
|
resp, svcErr := h.svc.Update(c.Request.Context(), id, body.Rating, body.FeedbackMessage, body.CsatReviewNotes)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
response.OK(c, resp)
|
|
}
|
|
|
|
// PublicGet retrieves a CSAT survey response by conversation UUID (no auth required).
|
|
// GET /public/api/v1/conversations/:conversation_uuid/csats
|
|
func (h *CsatSurveyHandler) PublicGet(c *gin.Context) {
|
|
conversationUUID := c.Param("conversation_uuid")
|
|
if conversationUUID == "" {
|
|
conversationUUID = c.Param("id")
|
|
}
|
|
if conversationUUID == "" {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "conversation_uuid is required")
|
|
return
|
|
}
|
|
|
|
resp, svcErr := h.svc.GetByConversationUUID(c.Request.Context(), conversationUUID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
response.OK(c, resp)
|
|
}
|
|
|
|
// PublicUpdate updates a CSAT survey response via conversation UUID (no auth required).
|
|
// POST /public/api/v1/conversations/:conversation_uuid/csats
|
|
func (h *CsatSurveyHandler) PublicUpdate(c *gin.Context) {
|
|
conversationUUID := c.Param("conversation_uuid")
|
|
if conversationUUID == "" {
|
|
conversationUUID = c.Param("id")
|
|
}
|
|
if conversationUUID == "" {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "conversation_uuid is required")
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
Rating int `json:"rating"`
|
|
FeedbackMessage string `json:"feedback_message"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
// Find existing response by UUID
|
|
resp, svcErr := h.svc.GetByConversationUUID(c.Request.Context(), conversationUUID)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
if svcErr := h.svc.UpdateResponse(c.Request.Context(), resp.ID, body.Rating, body.FeedbackMessage); svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"id": resp.ID,
|
|
"rating": body.Rating,
|
|
"feedback_message": body.FeedbackMessage,
|
|
})
|
|
}
|
|
|
|
// buildCsatFilter constructs a CsatListFilter from Gin query parameters.
|
|
func buildCsatFilter(c *gin.Context) automation.CsatListFilter {
|
|
filter := automation.CsatListFilter{
|
|
Page: 1,
|
|
PageSize: 25,
|
|
}
|
|
|
|
// agent_id filter
|
|
if v := c.Query("agent_id"); v != "" {
|
|
filter.AgentID = csatUintPtr(csatParseUint(v))
|
|
}
|
|
|
|
// since filter (RFC3339 timestamp)
|
|
if v := c.Query("since"); v != "" {
|
|
t, err := time.Parse(time.RFC3339, v)
|
|
if err == nil {
|
|
filter.Since = &t
|
|
}
|
|
}
|
|
|
|
// until filter (RFC3339 timestamp)
|
|
if v := c.Query("until"); v != "" {
|
|
t, err := time.Parse(time.RFC3339, v)
|
|
if err == nil {
|
|
filter.Until = &t
|
|
}
|
|
}
|
|
|
|
// pagination
|
|
if v := c.Query("page"); v != "" {
|
|
if n, err := csatParseUintFull(v); err == nil && n > 0 {
|
|
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
|
|
}
|
|
|
|
// csatParseUint parses a query parameter value as uint, returning 0 on failure.
|
|
func csatParseUint(val string) uint {
|
|
n, err := csatParseUintFull(val)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
// csatParseUintFull parses a query parameter value as uint with error.
|
|
func csatParseUintFull(val string) (uint, error) {
|
|
n, err := strconv.ParseUint(val, 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint(n), nil
|
|
}
|
|
|
|
// csatUintPtr returns a pointer to the given uint value.
|
|
func csatUintPtr(v uint) *uint {
|
|
return &v
|
|
}
|
|
|
|
// Download exports CSAT survey responses as CSV.
|
|
// GET /api/v1/accounts/:account_id/csat_survey_responses/download
|
|
// Reference: Chatwoot csat_survey_responses_controller#download — CSV format matches Chatwoot template
|
|
func (h *CsatSurveyHandler) Download(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
filter := buildCsatFilter(c)
|
|
// Download gets all records (no pagination limit)
|
|
filter.PageSize = 0
|
|
|
|
responses, _, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, filter)
|
|
if svcErr != nil {
|
|
handleServiceError(c, svcErr)
|
|
return
|
|
}
|
|
|
|
// Chatwoot CSV columns: agent_name, rating, feedback, contact_name, contact_email,
|
|
// contact_phone, conversation_link, recorded_at (+ review_notes for enterprise)
|
|
csv := "Agent Name,Rating,Feedback Message,Contact Name,Contact Email,Contact Phone Number,Conversation Link,Recorded At\n"
|
|
for _, r := range responses {
|
|
agentName := ""
|
|
if r.AssignedAgentID != nil {
|
|
// Lookup agent name via DB
|
|
var agent model.User
|
|
if err := h.svc.DB().Where("id = ?", *r.AssignedAgentID).First(&agent).Error; err == nil {
|
|
agentName = fmt.Sprintf("%s (%s)", agent.Name, agent.Email)
|
|
}
|
|
}
|
|
|
|
// Lookup contact details
|
|
var contact model.Contact
|
|
contactName := ""
|
|
contactEmail := ""
|
|
contactPhone := ""
|
|
if err := h.svc.DB().Where("id = ?", r.ContactID).First(&contact).Error; err == nil {
|
|
contactName = contact.Name
|
|
contactEmail = contact.Email
|
|
contactPhone = contact.PhoneNumber
|
|
}
|
|
|
|
// Lookup conversation display_id for link
|
|
var conv model.Conversation
|
|
conversationLink := ""
|
|
if err := h.svc.DB().Where("id = ?", r.ConversationID).First(&conv).Error; err == nil {
|
|
// Chatwoot format: /app/accounts/{account_id}/conversations/{display_id}
|
|
conversationLink = fmt.Sprintf("/app/accounts/%d/conversations/%d", accountID, conv.DisplayID)
|
|
}
|
|
|
|
csv += fmt.Sprintf("\"%s\",%d,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
|
|
agentName,
|
|
r.Rating,
|
|
sanitizeCSV(r.FeedbackMessage),
|
|
contactName,
|
|
contactEmail,
|
|
contactPhone,
|
|
conversationLink,
|
|
r.CreatedAt.Format(time.RFC3339),
|
|
)
|
|
}
|
|
|
|
c.Header("Content-Type", "text/csv")
|
|
c.Header("Content-Disposition", "attachment; filename=csat_report.csv")
|
|
c.String(http.StatusOK, csv)
|
|
}
|
|
|
|
// sanitizeCSV escapes double quotes and newlines for CSV safety.
|
|
func sanitizeCSV(s string) string {
|
|
s = strings.ReplaceAll(s, "\"", "\"\"")
|
|
s = strings.ReplaceAll(s, "\n", " ")
|
|
return s
|
|
}
|