Files

89 lines
3.5 KiB
Markdown
Raw Permalink Normal View History

# 12 — Support tickets backend
**Status:** Implemented (extends Support Center)
**Agent:** 12/20
**Date:** 2026-08-05
**Design:** [11-support-design.md](./11-support-design.md) · [11-support-api-contract.json](./11-support-api-contract.json)
## Summary
Additive support-desk backend on top of `025_support_center` + `029_staff_roles`:
- Customers: create / list / reply with **company + owner isolation** (unchanged contract).
- Staff desk (`RequireSupportDesk`): queue + claim visibility for `staff_role=support_staff`; full list for platform admins.
- Claim / release endpoints; assign force-fields remain admin-oriented.
- Agents directory via `staff_role` (maps API `is_support_agent` without a second boolean column).
- Migration `030_support_desk.sql`: `resolved_by_user_id`, CSAT table (ratings owned by agent 13), unassigned index, notification kinds.
## Schema
| Artifact | Purpose |
|----------|---------|
| `029_staff_roles.sql` | `users.staff_role``admin\|developer\|support_staff` |
| `027_capabilities_support_perf.sql` | assignee / status activity indexes |
| `030_support_desk.sql` | `resolved_by_user_id`, CSAT table + token cols, unassigned partial index, `ticket_claimed` / `csat_requested` notification kinds |
**ASSUMPTION:** Designs `is_support_agent` boolean is implemented as `staff_role='support_staff'` (roles agents landed first). API still exposes `is_support_agent` on agent DTOs / `/me` staff block.
## Auth
| Middleware | Who |
|------------|-----|
| `RequireSession` + `RequireCompany` | Customer ticket + notification routes |
| `RequireSupportDesk` | `/api/admin/support/tickets*` claim/release |
| `RequirePlatformAdmin` | `/api/admin/support/agents*` (+ other admin) |
`auth.ResolveStaffAccess`: `support_staff` → desk only; `admin`/`developer`/legacy `is_platform_admin` → full admin + desk.
## Visibility (queue + claim)
| Scope | Filter |
|-------|--------|
| `inbox` (agent default) | assignee IS NULL OR me; status ∈ open\|pending |
| `mine` | assignee = me |
| `unassigned` | assignee IS NULL; open\|pending |
| `all` (admin default) | optional status / company_id / assignee_id / q |
Agent `scope=all`**403**. Cross-assignee GET → **404**. Reply on another agents ticket → **409 already_claimed**.
Claim SQL (atomic):
```sql
UPDATE support_tickets
SET assignee_admin_user_id = $actor, updated_at = now()
WHERE id = $id AND assignee_admin_user_id IS NULL AND status IN ('open','pending');
```
## Key packages
| Path | Role |
|------|------|
| `internal/support/tickets.go` | CRUD, reply, update (+ `resolved_by_user_id` on resolve) |
| `internal/support/desk.go` | scopes, claim, release, `GetAdminForActor` |
| `internal/support/agents.go` | list/set support agents via `staff_role` |
| `internal/httpapi/support_handlers.go` | HTTP + staff visibility helpers |
| `internal/httpapi/admin_staff_handlers.go` | set agent |
| `internal/auth/staff.go` | StaffAccess resolution |
## Tests
```bash
cd apps/api
go test ./internal/support/ -count=1
# with DATABASE_URL:
go test ./internal/support/ -run 'TicketCRUDAuthOwnership|StaffQueueClaimRelease' -count=1
go test ./internal/httpapi/ -run Support -count=1
```
## Out of scope here
- CSAT submit/aggregate HTTP (agent 13 — schema prepared in `029`).
- Staff/customer UI (later agents).
- Support email stubs (optional; mailer unused by support MVP).
## Verification notes
- Customer list/get still require `company_id` + `created_by_user_id`.
- Internal notes never returned on customer GET.
- Pagination clamped 50 default / 200 max (`clampListBounds`).