Skip to main content

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 (403 otherwise; on create a non-staff assignee_id is ignored). The assignee must hold a role in the tenant (400 otherwise).

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, not 403, so ids can't be probed.
  • A comment's author_type comes from the caller's role kind, never from the request.

API​

MethodPathNotes
POST/ticketssubject, priority, description, customer_id, participants, assignee_id, author_id (staff only)
GET/ticketsPaginated; 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}/escalatetickets:escalate; moves to the next level and notifies the escalation matrix
GET / POST/tickets/{id}/commentsThe 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.