5.4 KiB
tickets — coordination, support, emergency
Client seam
client/src/services/tickets/·USE_TICKETS_MOCK = false(real) · 11 server ops Last verified: 2026-07-30 against commitd3ec723and swagger.v1.json (2026-07-29).
Threaded messaging between customer, nurse and staff. Also the emergency channel. The in-app notification feed is a separate domain — notifications.md.
Endpoints
| Method | Path | Verdict |
|---|---|---|
| GET | /api/v1/tickets |
wired · paginated · bookingId filter |
| POST | /api/v1/tickets |
wired |
| GET | /api/v1/tickets/{id} |
wired — stamps last_read_at |
| POST | /api/v1/tickets/{id}/messages |
wired · optional clientMessageId |
| POST | /api/v1/tickets/{id}/close |
wired |
| POST | /api/v1/tickets/{id}/reopen |
wired |
| POST | /api/v1/tickets/emergency |
unwired — no emergency entry point in the UI yet |
| POST | /api/v1/tickets/{id}/participants |
unwired |
| DELETE | /api/v1/tickets/{id}/participants/{userId} |
unwired |
| GET | /api/v1/admin/tickets |
wired · paginated — the staff queue |
| GET | /api/v1/admin/tickets/{id} |
wired — the staff thread, internal notes visible |
All [Authorize]; the two admin/tickets routes are DynamicPermission. Note the hardcoded nested
admin/ route segment — see index.md.
Phantom — 1
| Client call | REQ | Live? |
|---|---|---|
POST /api/v1/tickets/{id}/assign |
REQ-063 | Yes — this domain's mock is off. Gated behind TICKET_LIFECYCLE_ENABLED, default off, so nothing calls it today |
tickets/constants.tsis stale on this. It says the gate is off because "the backend has no close/reopen/assign routes yet (REQ-063)".closeandreopenexist and are wired. Onlyassignis missing. The gate could be turned on for close/reopen alone.
isInternal is a query-layer boundary
The hardest rule in this domain, and the easiest to get wrong in a UI:
TicketMessageDtocarriesisInternal, so the field is on the wire shape.- The filtering is not. A non-staff caller's thread query never returns an internal row, and a non-staff caller can never set one. Both are enforced at the query layer, in the handler — never in the UI.
- Consequence for the client: it must not model internal notes as "rows to hide". They do not arrive. A client-side filter would be a second, weaker gate that hides a leak rather than preventing one.
- Consequence for the server: any new ticket read must repeat the filter. There is no global interceptor doing it.
Shape rules the JSON does not express
- Unread is computed server-side against
last_read_at, which is stamped when the participant fetches the user-facing thread (GET /tickets/{id}) — not by a separate mark-read call. So opening a thread is what clears its badge.unreadCountcounts non-internal messages from others after that stamp, and is 0 on the admin queue by definition. clientMessageIdis optimistic-send idempotency (REQ-028): a retried send with the same key returns the original message and echoes the key back onPostMessageResult. This is what makes the client's retry-in-place send safe. It is not theIdempotency-Keyheader — it is a body field, and it is the only place in the API that works this way.- The message author is a role label, not a name — confirmed intentional, for privacy.
TicketMessageDtocarriessenderIdonly; the client derives the author label from the participant role. No raw identity is exposed, and none should be added. referenceCodeis the human-facing id shown to users and quoted in support. Treat it as opaque.bookingIdandrefundIdon the summary link a ticket to what it is about; thebookingIdquery filter is how the client jumps from a booking to its coordination thread.- Ticket bodies are encrypted at rest (refinement-phase-9).
Enums
| Vocabulary | Values | Note |
|---|---|---|
TicketStatus |
open closed |
matches Entities/Messaging/TicketCodes.cs |
TicketCategory |
coordination support refund emergency |
matches |
TicketAuthorRole |
customer nurse admin system |
the one cross-side mismatch in the API |
MessageSendStatus (client-only) |
sent sending failed |
optimistic-send UI state, never on the wire |
systemis client-only.TicketCodesdefinescustomer,nurse,admin. The client'sTicketAuthorRoleaddssystemfor platform-generated messages. Widening a union on the reading side is safe — the client can render a value the server never sends — but it means a reader of the client types would wrongly conclude the server emitssystem. Either the server should define it or the client should drop it; today it is a documented asymmetry.
Open REQs
| REQ | Status | Effect |
|---|---|---|
| REQ-028 | delivered | unreadCount + lastMessageAt on the summary, bookingId filter, clientMessageId dedupe, role-label authors — all present |
| REQ-059 | open | No last-message preview and no author role on the summary, and no unread-total read. The real inbox shows subject + status + time with no preview |
| REQ-060 | deferred | No message photo attachments. The affordance is designed and gated off |
| REQ-063 | open, narrower than filed | close and reopen are delivered and wired. Only assign is missing → 1 phantom |