73 lines
5.0 KiB
Markdown
73 lines
5.0 KiB
Markdown
# Research: Agent Ticket Queue
|
|||
|
|
|
||
|
|
## Decision: extend the existing `PATCH /admin/agents/:agentId`, don't add a new link endpoint
|
||
|
|
|
||
|
|
- **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 `Ticket`s (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.
|