Extends the existing PATCH /admin/agents/:agentId with an optional userId to finish wiring 010's Agent.userId link, and adds GET /agents/me/tickets + GET /admin/agents/:agentId/tickets sharing one ticketing/tickets service method, backed by a new Assignment @@index([agentId, isCurrent]). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2.4 KiB
2.4 KiB
Data Model: Agent Ticket Queue
Modified: Agent
No new column — userId/user already exist (010-identity-auth). This feature is the first to
actually write userId through an endpoint, and adds the supporting index below.
model Assignment {
// ...existing fields unchanged...
@@index([ticketId, isCurrent])
@@index([agentId, isCurrent]) // NEW — supports "current assignments for agent X"
@@map("assignments")
}
New (response-shape only, no new table): AssignedTicketSummary
A read projection, not a persisted entity — assembled per-request from Ticket joined to its
current Assignment, Product, CustomerReference, and (if present) SLARun.
| Field | Source | Notes |
|---|---|---|
id |
Ticket.id |
|
code |
Ticket.code |
e.g. ACME-2026-0042 |
status |
Ticket.status |
One of the 12 lifecycle states (003's own state machine) |
priority |
Ticket.priority |
Opaque string, as already modeled |
severity |
Ticket.severity |
Opaque string, as already modeled |
product |
Ticket.product |
{ id, externalProductId, name } |
customer |
Ticket.customer |
{ externalUserId, externalTenantId } — no PII beyond what 002's own CustomerReference already stores |
assignedAt |
Assignment.assignedAt |
The current assignment's start time |
sla |
SLARun (nullable) |
{ status, firstResponseDueAt, resolutionDueAt, breachedAt } or null if no SLARun exists yet for this ticket |
Validation / Business Rules
- Linking (
PATCH /admin/agents/:agentId's newuserIdfield):- The target
Usermust exist and have roleAGENT(FR-001). - No other
Agentrow may already have thatuserId(FR-001) — checked proactively before the write (research.md), not left to the database's own@uniqueconstraint to reject. userId: nullexplicitly unlinks (distinct from omitting the field, which leaves it unchanged — the existingupdateAgentSchemapattern for optional fields).
- The target
- Listing (
GET /agents/me/tickets,GET /admin/agents/:agentId/tickets):- Only
Assignment.isCurrent: truerows are considered (FR-003). - The agent-self route resolves
agentIdexclusively fromrequest.user.id→Agent.userIdlookup — never from any request input (FR-004). - A session with no linked
Agentrow throws a specificNotFoundError("No agent profile is linked to this account."), never an empty array (FR-006).
- Only