docs: plan and design artifacts for ticketing feature
/speckit-plan output for 003-ticketing: technical context and constitution gate check (all PASS), Phase 0 research (9 decisions: ticket code format, explicit 12-state lifecycle transition table, optimistic concurrency via version column, idempotency-key upsert reusing 002's CustomerReference pattern, explicit-reference-only recurring-problem linking, config-driven message visibility mapping, presigned-PUT attachment pipeline, a fail-closed placeholder malware scanner since none exists in this stack, and adding MinIO to Docker Compose for local/test S3-compatible storage), Phase 1 data model (Ticket/Problem/TicketMessage/TicketAttachment plus the inbound request -> ticket creation behavior), the lifecycle/messages/ attachments contract, and a 6-scenario quickstart. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
be2eb9907a
commit
4751cf3164
@@ -0,0 +1,58 @@
|
||||
# Contract: Ticket Lifecycle, Messages & Attachments
|
||||
|
||||
## Ticket creation (via the inbound trust boundary)
|
||||
|
||||
`POST /v1/support/requests` (002-saas-integration) now, after successful auth:
|
||||
|
||||
1. Resolve/create `Problem` (research.md's explicit-reference-only rule).
|
||||
2. Atomic create-or-fetch `Ticket` on `(productId, idempotencyKey)` — a retried request returns
|
||||
the same ticket, never a second one (FR-004/SC-002).
|
||||
3. Write a `SYSTEM_EVENT` message.
|
||||
4. Respond `202` with `{ ticketId, code, status, problemId }`.
|
||||
|
||||
**Guarantee**: no request that passes the trust boundary ever completes without a ticket existing
|
||||
(FR-001/SC-001) — ticket creation is synchronous within the same request, not queued.
|
||||
|
||||
## Ticket status transitions
|
||||
|
||||
`PATCH /tickets/:ticketId/status` — body `{ status: <new status>, expectedVersion: <int> }`.
|
||||
|
||||
| Step | Failure |
|
||||
|---|---|
|
||||
| Ticket exists and caller is tenant-authorized | `404` / `403` |
|
||||
| `expectedVersion` matches the ticket's current `version` | `409 CONFLICT` (FR-007/SC-007) — caller must re-read and retry |
|
||||
| Requested transition is a valid edge from the current status (research.md's table) | `400 INVALID_TRANSITION` |
|
||||
|
||||
On success: `status` and `version` (+1) update atomically; a `SYSTEM_EVENT` message records the
|
||||
transition.
|
||||
|
||||
## Messages
|
||||
|
||||
- `POST /tickets/:ticketId/messages` — body `{ type, body }` (`authorRef`/`visibleToCustomer`
|
||||
derived server-side, never accepted as input — FR-008).
|
||||
- `GET /tickets/:ticketId/messages` — the caller's scope (customer vs. agent/admin) determines
|
||||
which types are queried; a customer-scoped caller's query never includes
|
||||
`visibleToCustomer: false` rows (FR-009) — enforced in the repository's `WHERE` clause, not by
|
||||
filtering an already-fetched list.
|
||||
|
||||
## Attachments
|
||||
|
||||
1. `POST /tickets/:ticketId/attachments/upload-url` — body `{ fileName, mimeType, sizeBytes }`,
|
||||
validated against configured limits (FR-012) before a presigned PUT URL is returned. No
|
||||
`TicketAttachment` row exists yet at this point.
|
||||
2. Caller PUTs the file directly to the returned URL (file bytes never transit this API).
|
||||
3. `POST /tickets/:ticketId/attachments/:attachmentId/confirm` — creates the `TicketAttachment`
|
||||
row (`scanStatus: pending`) and enqueues the scan job on `attachments-queue`.
|
||||
4. `GET /tickets/:ticketId/attachments/:attachmentId/download-url` — returns a presigned GET URL
|
||||
only if `scanStatus == 'clean'`; otherwise `409` with the current scan status (FR-013/FR-014).
|
||||
|
||||
## Guarantees (callable contract)
|
||||
|
||||
1. **Ticket existence is synchronous with trust-boundary success** — never eventually-consistent.
|
||||
2. **Idempotency key reuse never creates a second ticket**, regardless of retry count (SC-002).
|
||||
3. **No internal-only message type is ever returned to a customer-scoped read**, verified per
|
||||
type (SC-003).
|
||||
4. **No attachment file byte ever reaches PostgreSQL** — only `storageKey` metadata (SC-004).
|
||||
5. **No attachment is downloadable before `scanStatus: clean`**, every time it's attempted
|
||||
(SC-005).
|
||||
6. **A concurrent, stale-version status update is rejected, never silently overwritten** (SC-007).
|
||||
Reference in New Issue
Block a user