Files
support_backend/specs/011-agent-ticket-queue/research.md
T
saqib mirandClaude Sonnet 5 23fadebb5c docs(011-agent-ticket-queue): plan, research, data model, contract, quickstart
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>
2026-09-07 12:58:25 +05:30

5.0 KiB

Research: Agent Ticket Queue

  • Decision: Add an optional userId: z.string().min(1).nullable().optional() to updateAgentSchema and handle it in AgentsService.update (proactively check the target User's role and any existing link before writing, same pre-check style as UsersService.create's duplicate-email check — see 010-identity-auth), rather than a dedicated PATCH /admin/agents/:agentId/link-user route.
  • Rationale: PATCH /admin/agents/:agentId already exists as the one place an agent's mutable fields are updated (name, teamId, active) — userId is exactly that kind of field, not a distinct workflow. A second endpoint would duplicate routing/auth wiring for no behavioral gain.
  • Alternatives considered: A dedicated /link-user endpoint — rejected as an unnecessary extra surface once the existing update endpoint's shape was checked and found to already fit.

Decision: proactive existence/role checks, not a caught unique-constraint error

  • Decision: Before writing userId, look up the target User (404 if it doesn't exist, a clear rejection if its role isn't AGENT) and look up any existing Agent already linked to that userId (a clear ConflictError if one exists and isn't this same agent) — the same pattern UsersService.create (010-identity-auth) already established for its own duplicate- email check, rather than letting Postgres's @unique constraint on Agent.userId throw and translating that error after the fact.
  • Rationale: Consistency with the one precedent this codebase already has for "reject a would-be duplicate before writing," and a clearer error message than parsing a raw PrismaClientKnownRequestError code.
  • Alternatives considered: Catch P2002 (unique constraint violation) and translate it — workable, but the proactive-check style already used by UsersService.create was preferred for consistency within the same codebase.

Decision: the ticket-summary query lives in ticketing/tickets, not orchestration/assignments

  • Decision: TicketsService (or a new TicketsRepository method) owns the new "tickets currently assigned to agent X" query, reading Assignment rows via orchestration/assignments's own already-public repository/service surface (its index.ts), not by reaching into orchestration's internals.
  • Rationale: The result is fundamentally a list of Tickets (with a projection of product/customer/SLA data) — ticketing/tickets already owns Ticket and its existing findById/findByCode methods; orchestration/assignments owns the assignment decision and history, not ticket listing. This mirrors 009's own precedent of problem-management reading ticketing's public surface rather than duplicating ticket state there.
  • Alternatives considered: A new cross-cutting reporting/dashboard module — rejected as premature; this is one query, not a new bounded concern (spec.md Assumptions explicitly rule out a general-purpose list/search endpoint).

Decision: one new Prisma index, Assignment @@index([agentId, isCurrent])

  • Decision: Add this composite index. The existing @@index([ticketId, isCurrent]) supports "is this ticket currently assigned, and to whom" (007's own original query shape); this feature's query is the mirror image — "which tickets is this agent currently assigned to" — and has no supporting index today.
  • Rationale: Without it, "all current assignments for agent X" is a sequential scan over the whole assignments table. Cheap, purely additive schema change; no data migration needed beyond the index build itself.
  • Alternatives considered: Rely on the existing [ticketId, isCurrent] index (Postgres can't use a composite index efficiently for a query that doesn't lead with its first column) — rejected; a plain sequential scan is the actual alternative, not this index.

Decision: two routes sharing one service method, not one route with an optional param

  • Decision: GET /agents/me/tickets (fastify.authenticate only — resolves the agent from request.user.id via the new Agent.userId link) and GET /admin/agents/:agentId/tickets (fastify.authenticate + requireRole('ADMIN') — resolves the agent directly from the URL param) both call the same TicketsService.listAssignedTo(agentId).
  • Rationale: FR-004 requires an agent's own call can never accept a client-supplied agentId — collapsing both into one route with an optional query param would make that invariant a runtime if instead of a routing-level guarantee. Two routes make "whose tickets" structurally unambiguous per caller type, matching 010's own precedent of GET /auth/me vs. an admin-only equivalent being distinct routes rather than one parameterized one.
  • Alternatives considered: GET /tickets?assignedAgentId=<id or 'me'> — rejected; makes FR-004's guarantee a body of validation logic rather than routing structure.