Adds autocannon as a devDependency for the load-test tooling (T001), and the shared schema migration T002 blocks: SLARun.version for optimistic concurrency, plus two Postgres partial unique indexes (assignments_one_current_per_ticket, escalation_events_ticket_rule_unique) guarding against the assignment and escalation races research.md documents. Applied directly to both the real dev DB and the throwaway test DB. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
758 lines
25 KiB
Plaintext
758 lines
25 KiB
Plaintext
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[]
|
|
errorCodeLookups ErrorCodeLookup[]
|
|
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 // <PRODUCT_CODE>-<YEAR>-<SEQUENCE> — 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-<PRODUCT>-<SEQ>, 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[]
|
|
lookups ErrorCodeLookup[]
|
|
|
|
@@unique([productId, code])
|
|
@@map("error_codes")
|
|
}
|
|
|
|
// 015-reporting-dashboards research.md §6: a durable, append-only audit row recording that a
|
|
// known-error-code lookup happened — 014-full-observability's own equivalent
|
|
// (supporthub_known_error_lookups_total) is a process-lifetime Prometheus counter, unusable for
|
|
// a historical "top errors" report. productId is denormalized from errorCode.productId so the
|
|
// Product dashboard's range query never needs to join back through ErrorCode just to filter.
|
|
model ErrorCodeLookup {
|
|
id String @id @default(cuid())
|
|
errorCodeId String
|
|
errorCode ErrorCode @relation(fields: [errorCodeId], references: [id])
|
|
productId String
|
|
product Product @relation(fields: [productId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([productId, createdAt])
|
|
@@map("error_code_lookups")
|
|
}
|
|
|
|
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?
|
|
|
|
// 016-load-concurrency-testing: optimistic-concurrency counter, identical convention to
|
|
// Ticket.version (003-ticketing) — guards pause/resume/complete/the breach sweep against
|
|
// racing each other and silently clobbering this run's state (research.md §2).
|
|
version Int @default(0)
|
|
|
|
@@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 | <agentId> | <adminId>
|
|
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")
|
|
}
|