# 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:** Design’s `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 agent’s 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`).