91 lines
2.6 KiB
Markdown
91 lines
2.6 KiB
Markdown
# 13 — Support ticket ratings / CSAT
|
||||
|
|
|
|||
|
|
**Owner:** agent 13
|
|||
|
|
**Depends on:** agent 12 schema (`030_support_desk.sql` → `support_csat_ratings`)
|
|||
|
|
**Status:** implemented (API + tests)
|
|||
|
|
|
|||
|
|
## Goal
|
|||
|
|
|
|||
|
|
Customers rate **resolved** or **closed** tickets once (score 1–5 + optional comment). Platform admins see aggregate CSAT (no PII).
|
|||
|
|
|
|||
|
|
## Schema (agent 12)
|
|||
|
|
|
|||
|
|
Table `support_csat_ratings`:
|
|||
|
|
|
|||
|
|
| Column | Notes |
|
|||
|
|
|--------|--------|
|
|||
|
|
| `ticket_id` | `UNIQUE` — one rating per ticket |
|
|||
|
|
| `company_id`, `user_id` | owner + tenant |
|
|||
|
|
| `score` | `SMALLINT` 1–5 |
|
|||
|
|
| `comment` | optional, max 2000 runes (truncated) |
|
|||
|
|
| `created_at` | UTC |
|
|||
|
|
|
|||
|
|
Ticket columns `csat_token_hash` / `csat_invite_sent_at` exist for optional public token flow (not wired in this agent).
|
|||
|
|
|
|||
|
|
## Rules
|
|||
|
|
|
|||
|
|
1. Only `created_by_user_id` may rate (others → `404`).
|
|||
|
|
2. Status must be `resolved` or `closed` else `400` (`ticket not eligible for rating`).
|
|||
|
|
3. Duplicate insert → `409` (`already rated`).
|
|||
|
|
4. Logs: `ticket_id` + `score` only — **never** comment, email, or names.
|
|||
|
|
|
|||
|
|
## APIs
|
|||
|
|
|
|||
|
|
| Method | Path | Auth | Response |
|
|||
|
|
|--------|------|------|----------|
|
|||
|
|
| `POST` | `/api/support/tickets/{id}/csat` | session + company | `201` `SupportCsat` |
|
|||
|
|
| `GET` | `/api/support/tickets/{id}` | customer | additive `csat` / `csat_eligible` |
|
|||
|
|
| `GET` | `/api/admin/support/tickets/{id}` | support desk | includes `csat` when present |
|
|||
|
|
| `GET` | `/api/admin/support/csat?from=&to=` | platform admin | aggregate |
|
|||
|
|
|
|||
|
|
### Customer submit body
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "score": 4, "comment": "optional" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Admin aggregate
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"total": 12,
|
|||
|
|
"average": 4.25,
|
|||
|
|
"distribution": { "1": 0, "2": 1, "3": 2, "4": 4, "5": 5 },
|
|||
|
|
"from": null,
|
|||
|
|
"to": null
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`from` / `to` are optional RFC3339 bounds on `created_at` (`to` exclusive).
|
|||
|
|
|
|||
|
|
## Code map
|
|||
|
|
|
|||
|
|
| Piece | Path |
|
|||
|
|
|-------|------|
|
|||
|
|
| Service | `apps/api/internal/support/ratings.go` |
|
|||
|
|
| Types | `CSATInput`, `CSATRating`, `CSATAggregate` in `types.go` |
|
|||
|
|
| Handlers | `apps/api/internal/httpapi/support_csat_handlers.go` |
|
|||
|
|
| Routes | customer POST csat; admin GET `/api/admin/support/csat` |
|
|||
|
|
| Unit tests | `ratings_test.go`, `support_csat_auth_test.go` |
|
|||
|
|
| Integration | `ratings_integration_test.go` (needs `DATABASE_URL` + goose) |
|
|||
|
|
|
|||
|
|
## Verification
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd apps/api
|
|||
|
|
go test ./internal/support/ -count=1 -run 'CSAT|NormalizeCSAT|ClientErrorCSAT'
|
|||
|
|
go test ./internal/httpapi/ -count=1 -run 'SupportCSAT|SupportTicketCRUDAuth'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
With DB migrated:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go test ./internal/support/ -count=1 -run TestSubmitCSATOwnershipAndOnce
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Out of scope (this agent)
|
|||
|
|
|
|||
|
|
- Public token CSAT (`POST /api/public/support/csat`)
|
|||
|
|
- User/staff UI panels (agents 14–15)
|
|||
|
|
- Email CSAT invite stubs
|