create mvp path
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# tickets — coordination, support, emergency
|
||||
|
||||
> Client seam `client/src/services/tickets/` · `USE_TICKETS_MOCK = false` (**real**) · 11 server ops
|
||||
> Last verified: 2026-07-30 against commit `d3ec723` and 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](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](index.md#route-shape-exceptions).
|
||||
|
||||
### 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.ts` is stale on this.** It says the gate is off because "the backend has no
|
||||
> close/reopen/assign routes yet (REQ-063)". `close` and `reopen` **exist and are wired.** Only `assign`
|
||||
> is 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:
|
||||
|
||||
- `TicketMessageDto` carries `isInternal`, 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. `unreadCount` counts non-internal messages from *others* after that
|
||||
stamp, and is **0 on the admin queue** by definition.
|
||||
- **`clientMessageId` is optimistic-send idempotency** (REQ-028): a retried send with the same key returns
|
||||
the original message and echoes the key back on `PostMessageResult`. This is what makes the client's
|
||||
retry-in-place send safe. It is **not** the `Idempotency-Key` header — 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.
|
||||
`TicketMessageDto` carries `senderId` only; the client derives the author label from the participant
|
||||
role. No raw identity is exposed, and none should be added.
|
||||
- **`referenceCode` is the human-facing id** shown to users and quoted in support. Treat it as opaque.
|
||||
- `bookingId` and `refundId` on the summary link a ticket to what it is about; the `bookingId` query
|
||||
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 |
|
||||
|
||||
> **`system` is client-only.** `TicketCodes` defines `customer`, `nurse`, `admin`. The client's
|
||||
> `TicketAuthorRole` adds `system` for 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 emits `system`. 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 |
|
||||
Reference in New Issue
Block a user