/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>
8.6 KiB
Implementation Plan: Ticket Creation, Messages & Attachments
Branch: 003-ticketing | Date: 2026-09-02 | Spec: spec.md
Input: Feature specification from specs/003-ticketing/spec.md
Summary
Add the Ticket/Problem/TicketMessage/TicketAttachment domain: creating a ticket (and its
problem) immediately from a validated inbound request (wiring into 002-saas-integration's
POST /v1/support/requests, which today only echoes trusted context back), a typed message
timeline with enforced internal-note privacy, and an attachment pipeline built on the
already-scaffolded S3-compatible storageService plus a new async malware-scan job on the
already-scaffolded attachments-queue. Idempotency-key enforcement (deferred from
002-saas-integration's FR-012) is implemented here since Ticket now exists.
Technical Context
Language/Version: TypeScript 5.4 / Node.js 20+.
Primary Dependencies: Prisma (new models), BullMQ (new attachment-scan job, using the
existing attachments-queue and QueueManager), @aws-sdk/client-s3 +
@aws-sdk/s3-request-presigner (already wired via storageService — no new dependency), Zod.
Storage: PostgreSQL via Prisma (new Ticket, Problem, TicketMessage, TicketAttachment
models per docs/06-database-schema.md) + S3-compatible object storage (AWS S3 in
test/production, MinIO locally — per the user's confirmed choice, matching doc 04's explicit
guidance and the existing storageConfig/storageService scaffold).
Testing: Vitest — unit tests for the state-machine transition table and message-visibility
mapping; integration tests for ticket creation (incl. idempotency and recurring-problem linking),
message read-scoping, and the attachment upload → scan → download flow, against a real
Postgres/Redis/S3-compatible target (MinIO via docker-compose.test.yml, extended by this
feature — see research.md).
Target Platform: Same Fastify modular monolith. Extends the already-scaffolded
src/modules/ticketing/{tickets,messages,attachments} modules (currently: tickets has a bare
GET /tickets stub; messages/attachments are unimplemented skeletons).
Project Type: Backend service — single project.
Performance Goals: Ticket creation (FR-001) must complete synchronously within the inbound request's own response — no async/eventual-consistency gap between "request accepted" and "ticket exists" (this is the whole point of Constitution Principle "ticket created immediately"). Malware scanning is explicitly asynchronous (SC-005 only requires it's enforced before download, not before upload completes).
Constraints: MUST NOT store attachment file bytes in PostgreSQL (FR-011); MUST NOT make an attachment downloadable before its scan clears (FR-013); ticket status updates MUST use optimistic concurrency (FR-007); every ticket/message/attachment query MUST be tenant-scoped (FR-015).
Scale/Scope: One inbound-flow change (002-saas-integration's stub handler becomes real ticket creation), full CRUD-ish surface for messages or a customer/agent to read, and an attachment upload/download surface. Explicitly excludes investigation/root-cause/solution/resolution (Phase 9) and AI diagnosis (Phase 4) per spec.md Assumptions.
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
| Principle / Section | Check | Result |
|---|---|---|
| I. SaaS Is the Sole Identity & Access Authority | Ticket/message/attachment queries are scoped by the already-validated reqContext (productId/customerId/tenantId) from 002-saas-integration's trust boundary — never a caller-supplied id alone (FR-015). |
PASS |
| II. Configuration Over Hardcoding | Message-type visibility mapping, attachment size/type limits, and scan-status gate are all defined as data/config, not scattered conditionals — see data-model.md. | PASS |
| III. Layered Architecture With Enforced Module Boundaries | Extends the existing tickets/messages/attachments modules through their own controller/service/repository layers and public index.ts; the scan job lives in src/jobs/attachments/ per the existing scaffold, calling the attachments module's repository through its public API only. |
PASS |
| IV. AI Recommends, Deterministic Policy Decides | Not applicable — no AI in this feature (explicitly deferred to Phase 4). | PASS — N/A |
| V. Evidence-Based Verification | Not applicable — resolution/verification is Phase 9. | PASS — N/A |
| VI. Durable Audit & History | Ticket status transitions and attachment scan-status changes are written as SYSTEM_EVENT ticket messages (visible per FR-008's type-driven visibility), giving a durable, queryable history without a separate audit mechanism for this feature's own state changes. |
PASS |
| VII. Concurrency-Safe, Durable Job Handling | Ticket status updates use optimistic concurrency (a version column, FR-007); the malware-scan job is idempotent (re-running it for an already-scanned attachment is a no-op); idempotency-key enforcement (FR-004) reuses the same atomic-upsert pattern as 002-saas-integration's CustomerReference.findOrCreate. |
PASS |
| VIII. Problem and Ticket Are Separate, Related Entities | Directly implements this principle (FR-003) — this is the feature that first creates both entities. | PASS |
| Technology & Platform Constraints | Uses only already-present dependencies (Prisma, BullMQ, AWS SDK, Zod) — no new runtime dependency. | PASS |
No violations requiring Complexity Tracking justification.
Post-Design Constitution Re-check
All gates above remain PASS after Phase 1 design. One design detail worth calling out: the
malware scanner (research.md) fails closed (defaults to infected, never clean) precisely
because Principle V's spirit ("evidence-based, not assumed") applies here even though this
feature's own scope is pre-Phase-9 — an attachment pipeline that silently marked everything
"clean" would be asserting a safety property with no evidence behind it.
Project Structure
Documentation (this feature)
specs/003-ticketing/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output
└── tasks.md # Phase 2 output (/speckit-tasks — not created here)
Source Code (repository root)
supporthub-api/
├── prisma/
│ └── schema.prisma # MODIFIED — add Ticket, Problem, TicketMessage,
│ TicketAttachment models per docs/06
├── src/
│ ├── modules/
│ │ ├── catalog/products/
│ │ │ └── routes/inbound-request.routes.ts # MODIFIED — actually create a ticket instead
│ │ │ of echoing context back
│ │ └── ticketing/
│ │ ├── tickets/ # EXTENDED (existing scaffold) — create/get/
│ │ │ ├── controller/ list/updateStatus, state machine, idempotency
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── routes/
│ │ │ ├── schema/
│ │ │ ├── mapper/
│ │ │ └── types/
│ │ ├── messages/ # EXTENDED (existing scaffold) — post/list,
│ │ │ └── ... visibility-scoped reads
│ │ └── attachments/ # EXTENDED (existing scaffold) — upload
│ │ └── ... (presign + confirm), scan-gated download
│ └── jobs/
│ └── attachments/ # EXTENDED (existing scaffold) — real malware
│ └── index.ts scan worker (pluggable scanner, see research.md)
└── tests/
├── unit/ticketing/ # state machine, visibility mapping
└── integration/ # creation/idempotency, messages, attachments
Structure Decision: Single project, extending the three already-scaffolded ticketing
submodules rather than restructuring them — docs/07-backend-architecture.md's module layout
already anticipated exactly this shape. The inbound-request handler from 002-saas-integration is
modified in place (it's the one integration point between "request trusted" and "ticket exists"),
not duplicated.
Complexity Tracking
No constitution violations — table intentionally omitted.