Files
support_backend/specs/003-ticketing/data-model.md
T
saqib mirandClaude Sonnet 5 4751cf3164 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>
2026-09-02 14:53:53 +05:30

5.0 KiB

Phase 1 Data Model: Ticket Creation, Messages & Attachments

All new models use cuid() ids, matching the convention established in 002-saas-integration. Relations to not-yet-existing models (AI sessions, assignments, SLA runs, escalation events, investigations, root causes, solutions — all later phases) are deliberately omitted for now and added when those phases introduce the models they'd point to; Prisma can't reference a model that doesn't exist.

Ticket

Field Type Notes
id String @id @default(cuid())
code String @unique <PRODUCT_CODE>-<YEAR>-<SEQUENCE> (research.md)
productId String FK → Product
problemId String FK → Problem
customerId String FK → CustomerReference (from 002-saas-integration) — the normalized reference
externalUserId String Denormalized copy, matches doc 06's literal shape for query convenience without a join
externalTenantId String Denormalized copy, same rationale
status String One of the 12 states in research.md's state machine; @default("NEW")
priority String Free-text for now — PriorityPolicy-driven derivation is Phase 6/orchestration, not this feature
severity String
categoryId String? FK → existing Category model (already in schema from the original scaffold)
idempotencyKey String? Research.md's idempotency mechanism
version Int @default(1) Optimistic concurrency (research.md)
createdAt / updatedAt DateTime

Constraints: @@unique([productId, idempotencyKey]) (nullable-excluded — two tickets with idempotencyKey: null don't conflict). Index on (productId, status) and (externalTenantId, externalUserId) for the query patterns FR-015 requires (tenant/user-scoped lookups).

Status transition rule: enforced entirely in the service layer against the explicit adjacency table in research.md — the column itself has no DB-level CHECK constraint beyond "is a known string," since Prisma doesn't model state machines natively and a CHECK constraint would need to be duplicated in code anyway for the "attempted from X" half of transition validation.

Problem

Field Type Notes
id String @id @default(cuid())
statement String
symptoms String
impact String?
productId String FK → Product
categoryId String? FK → existing Category model
severity String
customerImpact String?
businessImpact String?
environment String?
createdAt DateTime @default(now())

Relations added by this feature: tickets Ticket[] (1 problem : many tickets, Constitution Principle VIII).

TicketMessage

Field Type Notes
id String @id @default(cuid())
ticketId String FK → Ticket
type String CUSTOMER_MESSAGE | AI_MESSAGE | AGENT_MESSAGE | INTERNAL_NOTE | SYSTEM_EVENT | INVESTIGATION_NOTE | SOLUTION_NOTE
authorRef String agentId, "ai", "system", or externalUserId — never a local FK (Constitution Principle I)
body String
visibleToCustomer Boolean Set from the type→visibility map at write time (research.md) — never accepted as request input
createdAt DateTime @default(now())

Index: (ticketId, visibleToCustomer, createdAt) — the exact shape customer-scoped reads query on (FR-009).

TicketAttachment

Field Type Notes
id String @id @default(cuid())
ticketId String FK → Ticket
storageKey String S3/MinIO object key — never the file itself (FR-011)
fileName String Original filename, for display only
mimeType String Validated against an allow-list at upload-confirm time (FR-012)
sizeBytes Int Validated against a configured max at upload-confirm time
scanStatus String pending | clean | infected | rejected (research.md's MalwareScanner)
uploadedBy String agentId or externalUserId — same non-FK convention as TicketMessage.authorRef
createdAt DateTime @default(now())

Download rule: a presigned GET URL is generated only when scanStatus == 'clean' — enforced in the service layer before calling storageService.getPresignedUrl, never left to the caller to check first (FR-013/FR-014).

Inbound Request → Ticket Creation (behavior, not a new table)

Extends 002-saas-integration's inbound flow. After authenticateProductIntegration succeeds and populates request.reqContext, the route handler (previously a stub echoing context back) now:

  1. Resolves or creates a Problem (research.md's explicit-reference-only linking).
  2. Atomically creates (or, on idempotency-key conflict, fetches) the Ticket in NEW status.
  3. Writes a SYSTEM_EVENT TicketMessage recording the creation (Constitution Principle VI — durable audit trail via the message timeline itself).
  4. Returns the ticket's id/code/status to the caller (still 202, now backed by a real record instead of an echo).