Support Tickets
Code: app/models/ticket.py, ticket_comment.py, ticket_member.py; app/schemas/ticket.py,
ticket_comment.py; app/services/tickets.py, ticket_comments.py, escalation.py;
app/api/v1/routes/tickets.py.
A Ticket is a support request, usually about a Customer (customer_id, nullable - staff can
raise one between themselves). It can be created by hand, or from an email
(Mail). The whole feature is the tickets feature flag: a tenant without it
gets 403 on /tickets and /escalation.
Shape
tickets
tenant_id, customer_id (nullable)
subject, description (HTML from the editor), status, priority - plain strings, see enums
reported_by user id of the author, set once at creation
assignee_id user id of the one person working it, NULL = unassigned
escalation_level / escalated_at / escalated_by
created_at / updated_at / deleted_at (soft delete)
ticket_members (ticket_id, user_id) unique - CC / participants; extra visibility
ticket_comments the chat: author_type (operator | client | subscriber), author_id, body; immutable
User ids are plain strings (the users.id), not foreign keys.
TicketStatus:open,in_progress,resolved,closed.TicketPriority:low,medium,high,urgent.- Assignee vs members: the assignee is the owner; members are anyone else who should see the
ticket. Setting an assignee also adds them as a member, and notifies them. Only staff can assign
(
403otherwise; on create a non-staffassignee_idis ignored). The assignee must hold a role in the tenant (400otherwise).
Access
Permissions are tickets:read, tickets:write, tickets:escalate, tickets:manage (delete a
ticket / remove a member), granted by roles. What a caller may see is scoping by role kind
(_scoped_customer_ids, _require_owns_ticket):
- Staff see every ticket of the tenant.
- A customer sees only their own customer's tickets; a client (
member) sees the tickets of the customers of their accounts. Either also sees tickets they authored or were added to. - A ticket out of scope answers
404, not403, so ids can't be probed. - A comment's
author_typecomes from the caller's role kind, never from the request.
API
| Method | Path | Notes |
|---|---|---|
| POST | /tickets | subject, priority, description, customer_id, participants, assignee_id, author_id (staff only) |
| GET | /tickets | Paginated; filters status, priority, customer_id, assignee_id (repeatable), sort, order |
| GET / PATCH | /tickets/{id} | PATCH: subject, description, status, priority, assignee_id (null unassigns; omitted = unchanged) |
| DELETE | /tickets/{id} | tickets:manage |
| POST / DELETE | /tickets/{id}/members[/{user_id}] | Add: staff, the author, or a member. Remove: tickets:manage |
| POST | /tickets/{id}/escalate | tickets:escalate; moves to the next level and notifies the escalation matrix |
| GET / POST | /tickets/{id}/comments | The chat, oldest first |
The escalation matrix (/escalation/levels, /escalation/matrix CRUD; escalation:read /
escalation:write) says who is told per level, and each row carries an escalate_after_minutes
SLA window. Escalating a non-open ticket, or to a level that isn't higher, answers 400. When
ESCALATION_AUTO_ENABLED=true (default off), a scheduler job every
ESCALATION_AUTO_INTERVAL_MINUTES escalates open/in-progress tickets past that window
(escalation.escalate_overdue_tickets).
Not built
SLA breach reporting, tags, attachments, emailing a customer on a comment, bulk assignment, merging.