datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } enum UserRole { ADMIN AGENT CUSTOMER } model User { id String @id @default(uuid()) email String @unique name String role UserRole @default(CUSTOMER) passwordHash String // bcryptjs hash — never the plaintext password; see // specs/010-identity-auth/data-model.md active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt agent Agent? @@map("users") } model Product { id String @id @default(cuid()) externalProductId String @unique // reference into SaaS, not authoritative — see // .specify/memory/constitution.md Principle I name String supportEnabled Boolean @default(true) status String @default("active") // active | suspended | deprecated createdAt DateTime @default(now()) updatedAt DateTime @updatedAt categories Category[] integration ProductIntegration? problems Problem[] tickets Ticket[] knowledgeEntries KnowledgeEntry[] errorCodes ErrorCode[] knownIssues KnownIssue[] runbooks Runbook[] aiConfidencePolicies AIConfidencePolicy[] slaPolicies SLAPolicy[] escalationPolicies EscalationPolicy[] @@map("products") } model ProductIntegration { id String @id @default(cuid()) productId String @unique product Product @relation(fields: [productId], references: [id]) // AES-256-GCM-encrypted secret (ciphertext, not plaintext) — see // specs/002-saas-integration/research.md "Credential storage" credentialRef String previousCredentialRef String? previousCredentialExpiresAt DateTime? authMechanism String @default("signed_token") // free-text — not an enum, // so a future integration can use oauth2_client_credentials or mtls without a migration allowedScope Json // { tenantIds?: string[], allowAnyTenant?: boolean } rateLimitPerMinute Int @default(60) rateLimitPerUserPerMinute Int @default(20) status String @default("active") // active | suspended rotatedAt DateTime? revokedAt DateTime? createdAt DateTime @default(now()) @@map("product_integrations") } model CustomerReference { id String @id @default(cuid()) externalUserId String externalTenantId String createdAt DateTime @default(now()) tickets Ticket[] @@unique([externalUserId, externalTenantId]) @@map("customer_references") } model Category { id String @id @default(uuid()) productId String name String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt product Product @relation(fields: [productId], references: [id], onDelete: Cascade) problems Problem[] tickets Ticket[] slaPolicies SLAPolicy[] @@map("categories") } model Problem { id String @id @default(cuid()) statement String symptoms String impact String? productId String categoryId String? severity String customerImpact String? businessImpact String? environment String? createdAt DateTime @default(now()) product Product @relation(fields: [productId], references: [id]) category Category? @relation(fields: [categoryId], references: [id]) tickets Ticket[] investigations Investigation[] rootCauses RootCause[] solutions Solution[] @@map("problems") } model Ticket { id String @id @default(cuid()) code String @unique // -- — see // specs/003-ticketing/research.md "Ticket code format" productId String problemId String customerId String externalUserId String // denormalized copy of CustomerReference's field, for query externalTenantId String // convenience without a join — see data-model.md status String @default("NEW") // one of the 12 lifecycle states — see // specs/003-ticketing/research.md "Ticket lifecycle state machine" priority String severity String categoryId String? idempotencyKey String? version Int @default(1) // optimistic concurrency — see research.md createdAt DateTime @default(now()) updatedAt DateTime @updatedAt product Product @relation(fields: [productId], references: [id]) problem Problem @relation(fields: [problemId], references: [id]) customer CustomerReference @relation(fields: [customerId], references: [id]) category Category? @relation(fields: [categoryId], references: [id]) messages TicketMessage[] attachments TicketAttachment[] aiSessions AISupportSession[] assignments Assignment[] assignmentHistory AssignmentHistory[] slaRun SLARun? escalationEvents EscalationEvent[] resolution Resolution? @@unique([productId, idempotencyKey]) @@index([productId, status]) @@index([externalTenantId, externalUserId]) @@map("tickets") } model TicketMessage { id String @id @default(cuid()) ticketId String 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 body String visibleToCustomer Boolean // set from the type->visibility map at write time — see // specs/003-ticketing/research.md "Message type -> visibility mapping" createdAt DateTime @default(now()) ticket Ticket @relation(fields: [ticketId], references: [id]) @@index([ticketId, visibleToCustomer, createdAt]) @@map("ticket_messages") } model TicketAttachment { id String @id @default(cuid()) ticketId String storageKey String // S3/MinIO object key — never the file itself fileName String mimeType String sizeBytes Int scanStatus String @default("pending") // pending | clean | infected | rejected uploadedBy String // agentId or externalUserId — same non-FK convention as authorRef createdAt DateTime @default(now()) ticket Ticket @relation(fields: [ticketId], references: [id]) @@map("ticket_attachments") } model KnowledgeEntry { id String @id @default(cuid()) code String // KB--, e.g. KB-DQ-102 — shared across versions, // logical identifier is (code, version), NOT code alone — see // specs/004-product-knowledge/research.md "Versioning mechanism" version Int @default(1) isCurrentVersion Boolean @default(true) productId String feature String? type String // known_issue | faq | resolution_procedure | operations problem String? symptoms String? errorCode String? cause String? recommendedSolution String? verificationSteps String? escalationGuidance String? status String @default("draft") // draft | published | unpublished effectiveDate DateTime? categoryScope String[] validationStatus String @default("unvalidated") // unvalidated | validated owner String? lastReview DateTime? source String? createdAt DateTime @default(now()) product Product @relation(fields: [productId], references: [id]) @@unique([code, version]) @@index([productId, isCurrentVersion, status, effectiveDate]) @@map("knowledge_entries") } model ErrorCode { id String @id @default(cuid()) code String // e.g. LAYOUT_PARSE_042 productId String description String product Product @relation(fields: [productId], references: [id]) knownIssues KnownIssue[] @@unique([productId, code]) @@map("error_codes") } model KnownIssue { id String @id @default(cuid()) productId String errorCodeId String? description String status String @default("open") product Product @relation(fields: [productId], references: [id]) errorCode ErrorCode? @relation(fields: [errorCodeId], references: [id]) @@map("known_issues") } model Runbook { id String @id @default(cuid()) key String // e.g. PDF_HTML_CONVERSION_FAILURE — shared across versions, logical // identifier is (key, productId, version), NOT key alone — same convention as KnowledgeEntry version Int @default(1) isCurrentVersion Boolean @default(true) productId String steps Json // ordered array — order preserved exactly as authored active Boolean @default(true) product Product @relation(fields: [productId], references: [id]) @@unique([key, productId, version]) @@index([productId, key, isCurrentVersion, active]) @@map("runbooks") } model AuditLog { id String @id @default(cuid()) actor String // ProductIntegration id, agent id, "system", "ai", etc. — never a local // User foreign key; see specs/002-saas-integration/research.md "Aligning AuditLog" actorType String // customer | agent | admin | system | ai action String entityType String entityId String oldValue Json? newValue Json? reason String? metadata Json? createdAt DateTime @default(now()) @@map("audit_logs") } model AISupportSession { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) status String // analyzing | troubleshooting | verifying | resolved | escalated | // ended_by_agent — mirrored onto Ticket.status through the existing 003 state machine, see // specs/005-ai-support/research.md "AISupportSession.status drives Ticket.status" activeRunbookKey String? currentStepIndex Int? clarifyingQuestionsAsked Int @default(0) toolCallCount Int @default(0) startedAt DateTime @default(now()) endedAt DateTime? diagnoses AIDiagnosis[] interactions AIInteraction[] actions AIAction[] knowledgeRefs AIKnowledgeReference[] @@index([ticketId, status]) @@map("ai_support_sessions") } model AIDiagnosis { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) product String feature String? problemType String severity String confidence Float possibleCauses String[] createdAt DateTime @default(now()) @@index([sessionId]) @@map("ai_diagnoses") } model AIInteraction { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) role String // customer | ai content String createdAt DateTime @default(now()) @@index([sessionId, createdAt]) @@map("ai_interactions") } model AIAction { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) toolName String input Json riskLevel String // low | medium | high — copied from the registry at evaluation time evaluationOutcome String // approved | pending_approval | refused refusalReason String? approvedBy String? // system-policy | agentId | null while pending_approval createdAt DateTime @default(now()) result AIActionResult? @@index([sessionId, createdAt]) @@map("ai_actions") } model AIActionResult { id String @id @default(cuid()) actionId String @unique action AIAction @relation(fields: [actionId], references: [id]) output Json status String // success | failed createdAt DateTime @default(now()) @@map("ai_action_results") } model AIKnowledgeReference { id String @id @default(cuid()) sessionId String session AISupportSession @relation(fields: [sessionId], references: [id]) knowledgeId String // KnowledgeEntry.id — resolved through ai-support/knowledge's public // index.ts, not a cross-module DB-level FK (Constitution Principle III) relevanceScore Float? createdAt DateTime @default(now()) @@index([sessionId]) @@map("ai_knowledge_references") } model AIConfidencePolicy { id String @id @default(cuid()) productId String? // null = system-wide default row product Product? @relation(fields: [productId], references: [id]) categoryId String? // null = applies to every category of productId highThreshold Float lowThreshold Float maxClarifyingQuestions Int updatedAt DateTime @updatedAt @@unique([productId, categoryId]) @@map("ai_confidence_policies") } model Team { id String @id @default(cuid()) name String active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt agents Agent[] hierarchyNodes HierarchyNode[] @@map("teams") } model Agent { id String @id @default(cuid()) teamId String team Team @relation(fields: [teamId], references: [id]) name String active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Nullable link to the login identity this routing/skills profile belongs to — schema // capability only, no workflow sets it yet; see specs/010-identity-auth/research.md. userId String? @unique user User? @relation(fields: [userId], references: [id]) skills AgentSkill[] availability AgentAvailability? assignments Assignment[] @@index([teamId, active]) @@map("agents") } model AgentSkill { id String @id @default(cuid()) agentId String agent Agent @relation(fields: [agentId], references: [id]) skillTag String level Int // proficiency, used by a future SKILL_BASED assignment strategy — not // interpreted by this feature @@unique([agentId, skillTag]) @@map("agent_skills") } model AgentAvailability { id String @id @default(cuid()) agentId String @unique agent Agent @relation(fields: [agentId], references: [id]) status String // available | busy | away | offline — validated at the schema layer workingHours Json // per business calendar — opaque to this feature currentLoad Int @default(0) updatedAt DateTime @updatedAt // Last-write-wins on purpose — see specs/006-support-organization/research.md "Availability // concurrency"; no expectedVersion field here, unlike Ticket.status/KnowledgeEntry.version. @@map("agent_availability") } model HierarchyNode { id String @id @default(cuid()) name String parentId String? parent HierarchyNode? @relation("HierarchyTree", fields: [parentId], references: [id]) children HierarchyNode[] @relation("HierarchyTree") order Int teamId String? team Team? @relation(fields: [teamId], references: [id]) skills String[] productScope String[] // external product ids; empty = matches every product categoryScope String[] // free text; empty = matches every category priorityScope String[] // free text; empty = matches every priority assignmentStrategy String // free-text reference — no real strategy table exists yet (Phase 7) slaPolicyId String? // free-text reference — no SlaPolicy table exists yet (Phase 8) escalationPolicyId String? // free-text reference — no EscalationPolicy table exists yet entryConditions Json? // opaque rule expression — stored, not evaluated, by this feature exitConditions Json? // opaque rule expression — stored, not evaluated, by this feature active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt escalationRules EscalationRule[] @@index([parentId, order]) @@index([active]) @@map("hierarchy_nodes") } model Assignment { id String @id @default(cuid()) ticketId String // not unique — one row per assignment period, see // specs/007-orchestration-assignment/research.md ticket Ticket @relation(fields: [ticketId], references: [id]) agentId String agent Agent @relation(fields: [agentId], references: [id]) strategy String // ROUND_ROBIN | LEAST_LOADED | SKILL_BASED | MANUAL | DIRECT reason String? isCurrent Boolean @default(true) assignedAt DateTime @default(now()) unassignedAt DateTime? @@index([ticketId, isCurrent]) @@index([agentId, isCurrent]) @@map("assignments") } model AssignmentHistory { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) agentId String? // null for a "no eligible agent" outcome — FR-008 action String // assigned | reassigned | unassigned strategy String reason String? actor String // system | agentId | adminId createdAt DateTime @default(now()) @@index([ticketId, createdAt]) @@map("assignment_history") } model SLAPolicy { id String @id @default(cuid()) name String productId String? // wildcard when null — see data-model.md "Resolution" product Product? @relation(fields: [productId], references: [id]) categoryId String? category Category? @relation(fields: [categoryId], references: [id]) problemTypeId String? // free-text — no ProblemType table exists in this codebase priority String? // free-text, matches Ticket.priority firstResponseMinutes Int investigationMinutes Int? // stored per doc06; not read by this feature (spec.md Assumptions) resolutionMinutes Int customerResponseMinutes Int? // stored per doc06; not read by this feature (spec.md Assumptions) businessCalendarId String? // null = 24/7, no exclusions — an explicit policy choice businessCalendar BusinessCalendar? @relation(fields: [businessCalendarId], references: [id]) active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt slaRuns SLARun[] @@index([productId, categoryId, active]) @@map("sla_policies") } model SLARun { id String @id @default(cuid()) ticketId String @unique // one run per ticket — no reopen-cycle support (spec.md Assumptions) ticket Ticket @relation(fields: [ticketId], references: [id]) policyId String policy SLAPolicy @relation(fields: [policyId], references: [id]) firstResponseDueAt DateTime? resolutionDueAt DateTime? status String // running | paused | warning | breached | completed pausedAt DateTime? resumedAt DateTime? breachedAt DateTime? // Additive refinement beyond doc06 (research.md/data-model.md): records a first-response // breach separately from the resolution-timer breach status above, and doubles as the // idempotency guard for the breach-detection sweep (never re-fires on the same run). firstResponseBreachedAt DateTime? completedAt DateTime? @@index([status, resolutionDueAt]) @@index([status, firstResponseDueAt]) @@map("sla_runs") } model BusinessCalendar { id String @id @default(cuid()) name String timezone String // IANA zone name, e.g. "America/New_York" workingHours Json // { mon?: {start,end}, tue?: ..., ... } — see research.md holidays Holiday[] policies SLAPolicy[] @@map("business_calendars") } model Holiday { id String @id @default(cuid()) calendarId String calendar BusinessCalendar @relation(fields: [calendarId], references: [id], onDelete: Cascade) date DateTime // compared by calendar date only, in the calendar's own timezone description String? @@index([calendarId, date]) @@map("holidays") } model EscalationPolicy { id String @id @default(cuid()) name String productId String? // wildcard (global) when null — see research.md "Escalation policy resolution" product Product? @relation(fields: [productId], references: [id]) active Boolean @default(true) rules EscalationRule[] @@index([productId, active]) @@map("escalation_policies") } model EscalationRule { id String @id @default(cuid()) policyId String policy EscalationPolicy @relation(fields: [policyId], references: [id]) triggerType String // one of doc05 §6's 10 values; only resolution_breach/first_response_breach // are ever evaluated by this feature — the other 8 are valid, stored, inert config // (research.md) condition Json // stored, not evaluated, by this feature (research.md) targetNodeId String targetNode HierarchyNode @relation(fields: [targetNodeId], references: [id]) notify Json // who/how to notify — stored and returned only, no delivery mechanism exists active Boolean @default(true) @@index([policyId, triggerType, active]) @@map("escalation_rules") } model EscalationEvent { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id]) ruleId String? // null for a manual escalation or a breach with no matching rule fromNodeId String? toNodeId String? reason String triggeredBy String // system | | createdAt DateTime @default(now()) @@index([ticketId, createdAt]) @@map("escalation_events") } model Investigation { id String @id @default(cuid()) problemId String problem Problem @relation(fields: [problemId], references: [id]) investigator String findings Json evidence Json? internalNotes String? // never exposed on a customer-facing read — see // specs/009-problem-resolution/spec.md FR-003 status String @default("open") // open | complete createdAt DateTime @default(now()) @@index([problemId, createdAt]) @@map("investigations") } model RootCause { id String @id @default(cuid()) problemId String problem Problem @relation(fields: [problemId], references: [id]) type String // technical | configuration | external_dependency | business | // contributing_factor description String createdAt DateTime @default(now()) @@index([problemId, createdAt]) @@map("root_causes") } model Solution { id String @id @default(cuid()) problemId String problem Problem @relation(fields: [problemId], references: [id]) proposed String approved Boolean @default(false) createdAt DateTime @default(now()) implementation SolutionImplementation? verification SolutionVerification? @@index([problemId, createdAt]) @@map("solutions") } model SolutionImplementation { id String @id @default(cuid()) solutionId String @unique solution Solution @relation(fields: [solutionId], references: [id]) notes String? implementedBy String implementedAt DateTime @default(now()) @@map("solution_implementations") } model SolutionVerification { id String @id @default(cuid()) solutionId String @unique solution Solution @relation(fields: [solutionId], references: [id]) method String // automated | technical_test | customer_confirmation | agent_confirmation result String // success | failed evidence Json? verifiedAt DateTime @default(now()) @@map("solution_verifications") } model Resolution { id String @id @default(cuid()) ticketId String @unique ticket Ticket @relation(fields: [ticketId], references: [id]) outcome String resolvedBy String // "ai" | agentId — see specs/009-problem-resolution/data-model.md resolvedAt DateTime @default(now()) @@map("resolutions") }