Full system blueprint (docs 01-10): product vision, integration & security, AI support architecture, ticketing & problem management, orchestration/SLA/escalation, database schema, backend/frontend architecture, testing/observability/CI-CD, and the implementation roadmap. This is the pre-implementation design reference the codebase is being built against. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
550 lines
17 KiB
Markdown
550 lines
17 KiB
Markdown
# 06 — Database Schema
|
|
|
|
**Primary database:** PostgreSQL. **ORM:** Prisma. All schema below is conceptual/pseudo-Prisma — refine field types and add indexes during Phase 1 modeling (see [10 — Implementation Roadmap](./10-implementation-roadmap.md)).
|
|
|
|
Cross-cutting requirements for every table below:
|
|
- Migrations tracked in version control
|
|
- Indexes on every foreign key and every field used in ticket/queue filtering
|
|
- Unique constraints where the spec implies natural keys (e.g. `productId` + credential)
|
|
- Optimistic or transactional concurrency control wherever two actors could race (assignment, SLA state, hierarchy edits)
|
|
- `createdAt`/`updatedAt` on every table; soft-delete or status field where records must never disappear (audit, escalation events)
|
|
|
|
---
|
|
|
|
## Domain: Integration / Catalog
|
|
|
|
```prisma
|
|
model Product {
|
|
id String @id @default(cuid())
|
|
externalProductId String @unique // reference into SaaS, not authoritative
|
|
name String
|
|
supportEnabled Boolean @default(true)
|
|
status String // active | suspended | deprecated
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
integration ProductIntegration?
|
|
knowledgeEntries KnowledgeEntry[]
|
|
runbooks Runbook[]
|
|
tickets Ticket[]
|
|
}
|
|
|
|
model ProductIntegration {
|
|
id String @id @default(cuid())
|
|
productId String @unique
|
|
product Product @relation(fields: [productId], references: [id])
|
|
credentialRef String // pointer into secret manager, never the raw secret
|
|
authMechanism String // signed_token | oauth2_client_credentials | mtls
|
|
allowedScope Json // structured scope definition
|
|
rotatedAt DateTime?
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model CustomerReference {
|
|
id String @id @default(cuid())
|
|
externalUserId String
|
|
externalTenantId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([externalUserId, externalTenantId])
|
|
}
|
|
```
|
|
|
|
## Domain: Ticketing
|
|
|
|
```prisma
|
|
model Ticket {
|
|
id String @id @default(cuid())
|
|
code String @unique // e.g. DQB-2026-00567
|
|
productId String
|
|
product Product @relation(fields: [productId], references: [id])
|
|
problemId String
|
|
problem Problem @relation(fields: [problemId], references: [id])
|
|
externalUserId String
|
|
externalTenantId String
|
|
status String // NEW, AI_ANALYZING, AI_TROUBLESHOOTING, AI_VERIFYING,
|
|
// AI_RESOLVED, HUMAN_ESCALATION, IN_PROGRESS,
|
|
// WAITING_FOR_CUSTOMER, RESOLUTION_PENDING_CUSTOMER,
|
|
// RESOLVED, CLOSED, REOPENED
|
|
priority String
|
|
severity String
|
|
categoryId String?
|
|
problemTypeId String?
|
|
assignmentId String?
|
|
slaRunId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
messages TicketMessage[]
|
|
attachments TicketAttachment[]
|
|
aiSessions AISupportSession[]
|
|
assignments Assignment[]
|
|
escalationEvents EscalationEvent[]
|
|
}
|
|
|
|
model Problem {
|
|
id String @id @default(cuid())
|
|
statement String
|
|
symptoms String
|
|
impact String?
|
|
productId String
|
|
featureId String?
|
|
categoryId String?
|
|
problemTypeId String?
|
|
severity String
|
|
customerImpact String?
|
|
businessImpact String?
|
|
environment String?
|
|
createdAt DateTime @default(now())
|
|
|
|
tickets Ticket[]
|
|
investigations Investigation[]
|
|
rootCauses RootCause[]
|
|
solutions Solution[]
|
|
}
|
|
|
|
model TicketMessage {
|
|
id String @id @default(cuid())
|
|
ticketId String
|
|
ticket Ticket @relation(fields: [ticketId], references: [id])
|
|
type String // CUSTOMER_MESSAGE, AI_MESSAGE, AGENT_MESSAGE, INTERNAL_NOTE,
|
|
// SYSTEM_EVENT, INVESTIGATION_NOTE, SOLUTION_NOTE
|
|
authorRef String // agentId, "ai", or externalUserId
|
|
body String
|
|
visibleToCustomer Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model TicketAttachment {
|
|
id String @id @default(cuid())
|
|
ticketId String
|
|
ticket Ticket @relation(fields: [ticketId], references: [id])
|
|
storageKey String // S3/MinIO object key, not the file itself
|
|
fileName String
|
|
mimeType String
|
|
sizeBytes Int
|
|
scanStatus String // pending | clean | infected | rejected
|
|
uploadedBy String
|
|
createdAt DateTime @default(now())
|
|
}
|
|
```
|
|
|
|
## Domain: Category / Problem Type / Priority
|
|
|
|
```prisma
|
|
model Category { id String @id @default(cuid()) name String productId String? active Boolean @default(true) }
|
|
model ProblemType { id String @id @default(cuid()) name String categoryId String? active Boolean @default(true) }
|
|
model PriorityPolicy {
|
|
id String @id @default(cuid())
|
|
name String
|
|
productId String?
|
|
categoryId String?
|
|
rules Json // structured priority derivation rules
|
|
active Boolean @default(true)
|
|
}
|
|
```
|
|
|
|
## Domain: Support Hierarchy / Teams / Agents
|
|
|
|
```prisma
|
|
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?
|
|
skills String[]
|
|
productScope String[]
|
|
categoryScope String[]
|
|
priorityScope String[]
|
|
assignmentStrategy String
|
|
slaPolicyId String?
|
|
escalationPolicyId String?
|
|
entryConditions Json?
|
|
exitConditions Json?
|
|
active Boolean @default(true)
|
|
}
|
|
|
|
model Team {
|
|
id String @id @default(cuid())
|
|
name String
|
|
active Boolean @default(true)
|
|
agents Agent[]
|
|
}
|
|
|
|
model Agent {
|
|
id String @id @default(cuid())
|
|
teamId String
|
|
team Team @relation(fields: [teamId], references: [id])
|
|
name String
|
|
active Boolean @default(true)
|
|
skills AgentSkill[]
|
|
availability AgentAvailability?
|
|
}
|
|
|
|
model AgentSkill {
|
|
id String @id @default(cuid())
|
|
agentId String
|
|
agent Agent @relation(fields: [agentId], references: [id])
|
|
skillTag String
|
|
level Int // proficiency, used by SKILL_BASED strategy
|
|
}
|
|
|
|
model AgentAvailability {
|
|
id String @id @default(cuid())
|
|
agentId String @unique
|
|
agent Agent @relation(fields: [agentId], references: [id])
|
|
status String // available | busy | away | offline
|
|
workingHours Json // per business calendar
|
|
currentLoad Int @default(0)
|
|
}
|
|
```
|
|
|
|
## Domain: Assignment
|
|
|
|
```prisma
|
|
model Assignment {
|
|
id String @id @default(cuid())
|
|
ticketId String
|
|
ticket Ticket @relation(fields: [ticketId], references: [id])
|
|
agentId String
|
|
strategy String
|
|
assignedAt DateTime @default(now())
|
|
unassignedAt DateTime?
|
|
reason String?
|
|
}
|
|
|
|
model AssignmentHistory {
|
|
id String @id @default(cuid())
|
|
ticketId String
|
|
agentId String?
|
|
action String // assigned | reassigned | unassigned
|
|
strategy String
|
|
reason String?
|
|
actor String // system | agentId | adminId
|
|
createdAt DateTime @default(now())
|
|
}
|
|
```
|
|
|
|
## Domain: SLA
|
|
|
|
```prisma
|
|
model SLAPolicy {
|
|
id String @id @default(cuid())
|
|
name String
|
|
productId String?
|
|
categoryId String?
|
|
problemTypeId String?
|
|
priority String?
|
|
firstResponseMinutes Int
|
|
investigationMinutes Int?
|
|
resolutionMinutes Int
|
|
customerResponseMinutes Int?
|
|
businessCalendarId String?
|
|
active Boolean @default(true)
|
|
}
|
|
|
|
model SLARun {
|
|
id String @id @default(cuid())
|
|
ticketId String @unique
|
|
policyId String
|
|
firstResponseDueAt DateTime?
|
|
resolutionDueAt DateTime?
|
|
status String // running | paused | warning | breached | completed
|
|
pausedAt DateTime?
|
|
resumedAt DateTime?
|
|
breachedAt DateTime?
|
|
completedAt DateTime?
|
|
}
|
|
|
|
model BusinessCalendar {
|
|
id String @id @default(cuid())
|
|
name String
|
|
timezone String
|
|
workingHours Json
|
|
holidays Holiday[]
|
|
}
|
|
|
|
model Holiday {
|
|
id String @id @default(cuid())
|
|
calendarId String
|
|
calendar BusinessCalendar @relation(fields: [calendarId], references: [id])
|
|
date DateTime
|
|
description String?
|
|
}
|
|
```
|
|
|
|
## Domain: Escalation
|
|
|
|
```prisma
|
|
model EscalationPolicy {
|
|
id String @id @default(cuid())
|
|
name String
|
|
productId String?
|
|
active Boolean @default(true)
|
|
rules EscalationRule[]
|
|
}
|
|
|
|
model EscalationRule {
|
|
id String @id @default(cuid())
|
|
policyId String
|
|
policy EscalationPolicy @relation(fields: [policyId], references: [id])
|
|
triggerType String // first_response_breach | resolution_breach | inactivity |
|
|
// priority_increase | customer_escalation | repeated_reopen |
|
|
// manual | product_defect | dependency_timeout | critical_incident
|
|
condition Json
|
|
targetNodeId String
|
|
notify Json // who/how to notify
|
|
active Boolean @default(true)
|
|
}
|
|
|
|
model EscalationEvent {
|
|
id String @id @default(cuid())
|
|
ticketId String
|
|
ticket Ticket @relation(fields: [ticketId], references: [id])
|
|
ruleId String?
|
|
fromNodeId String?
|
|
toNodeId String?
|
|
reason String
|
|
triggeredBy String // system | agentId | customer
|
|
createdAt DateTime @default(now())
|
|
}
|
|
```
|
|
|
|
## Domain: Problem Resolution
|
|
|
|
```prisma
|
|
model Investigation {
|
|
id String @id @default(cuid())
|
|
problemId String
|
|
problem Problem @relation(fields: [problemId], references: [id])
|
|
investigator String
|
|
findings Json
|
|
evidence Json?
|
|
internalNotes String?
|
|
status String // open | complete
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
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?
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
model Resolution {
|
|
id String @id @default(cuid())
|
|
ticketId String @unique
|
|
outcome String
|
|
resolvedBy String // "ai" | agentId
|
|
resolvedAt DateTime @default(now())
|
|
}
|
|
```
|
|
|
|
## Domain: AI Support
|
|
|
|
```prisma
|
|
model AISupportSession {
|
|
id String @id @default(cuid())
|
|
ticketId String
|
|
ticket Ticket @relation(fields: [ticketId], references: [id])
|
|
status String // analyzing | troubleshooting | verifying | resolved | escalated
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
|
|
diagnoses AIDiagnosis[]
|
|
interactions AIInteraction[]
|
|
actions AIAction[]
|
|
knowledgeRefs AIKnowledgeReference[]
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
model AIRunbook {
|
|
id String @id @default(cuid())
|
|
key String // e.g. PDF_HTML_CONVERSION_FAILURE
|
|
productId String
|
|
steps Json // ordered, versioned step definitions
|
|
active Boolean @default(true)
|
|
}
|
|
|
|
model AIAction {
|
|
id String @id @default(cuid())
|
|
sessionId String
|
|
session AISupportSession @relation(fields: [sessionId], references: [id])
|
|
toolName String
|
|
input Json
|
|
riskLevel String
|
|
approvedBy String? // system-policy | agentId, when human approval required
|
|
createdAt DateTime @default(now())
|
|
|
|
result AIActionResult?
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
model AIKnowledgeReference {
|
|
id String @id @default(cuid())
|
|
sessionId String
|
|
session AISupportSession @relation(fields: [sessionId], references: [id])
|
|
knowledgeId String
|
|
relevanceScore Float?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
```
|
|
|
|
## Domain: Knowledge
|
|
|
|
```prisma
|
|
model KnowledgeEntry {
|
|
id String @id @default(cuid())
|
|
code String @unique // e.g. KB-DQ-102
|
|
productId String
|
|
product Product @relation(fields: [productId], references: [id])
|
|
feature String?
|
|
type String // known_issue | faq | resolution_procedure | operations
|
|
problem String?
|
|
symptoms String?
|
|
errorCode String?
|
|
cause String?
|
|
recommendedSolution String?
|
|
verificationSteps String?
|
|
escalationGuidance String?
|
|
version Int @default(1)
|
|
status String // draft | published | unpublished
|
|
effectiveDate DateTime?
|
|
categoryScope String[]
|
|
validationStatus String // unvalidated | validated
|
|
owner String?
|
|
lastReview DateTime?
|
|
source String?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model KnownIssue {
|
|
id String @id @default(cuid())
|
|
productId String
|
|
errorCodeId String?
|
|
description String
|
|
status String
|
|
}
|
|
|
|
model ErrorCode {
|
|
id String @id @default(cuid())
|
|
code String @unique // e.g. LAYOUT_PARSE_042
|
|
productId String
|
|
description String
|
|
}
|
|
|
|
model Runbook {
|
|
id String @id @default(cuid())
|
|
key String
|
|
productId String
|
|
product Product @relation(fields: [productId], references: [id])
|
|
steps Json
|
|
version Int @default(1)
|
|
active Boolean @default(true)
|
|
}
|
|
```
|
|
|
|
## Domain: Platform
|
|
|
|
```prisma
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
recipientRef String
|
|
channel String // in_app | email | push
|
|
event String
|
|
payload Json
|
|
status String // queued | sent | failed
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
actor String
|
|
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())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Notes on modeling decisions
|
|
|
|
- **`Ticket` vs `Problem` are always separate tables** with a many-tickets-to-one-problem relationship, per [04](./04-ticketing-and-problem-management.md#2-problem-is-first-class--separate-from-ticket).
|
|
- **`Investigation`, `RootCause`, `Solution`, `SolutionVerification`, `Resolution` are five distinct models**, not one "resolution notes" text field — this is intentional per the spec and enables reporting on each stage independently.
|
|
- **`AuditLog` should be append-only** at the application layer: no update/delete code paths against this table, ever.
|
|
- **SLA timing must never be computed from `SLARun.createdAt` alone** — always resolve through the linked `BusinessCalendar`/`Holiday` records at read time or via a durable recompute job.
|