Files
livedash-node/prisma/schema.prisma
Kaj Kowalski cd05fc8648 fix: resolve Prettier markdown code block parsing errors
- Fix syntax errors in skills markdown files (.github/skills, .opencode/skills)
- Change typescript to tsx for code blocks with JSX
- Replace ellipsis (...) in array examples with valid syntax
- Separate CSS from TypeScript into distinct code blocks
- Convert JavaScript object examples to valid JSON in docs
- Fix enum definitions with proper comma separation
2026-01-20 21:09:29 +01:00

399 lines
14 KiB
Plaintext

generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_URL_DIRECT")
}
/// *
/// * COMPANY (multi-tenant root)
/// * Root entity for multi-tenant architecture
/// * Each company has isolated data with own users, sessions, and AI model configurations
model Company {
id String @id @default(uuid())
/// Company name for display and filtering
name String @db.VarChar(255)
/// Company status for suspension/activation
status CompanyStatus @default(ACTIVE)
/// URL endpoint for CSV data import
csvUrl String
/// Optional HTTP auth username for CSV endpoint
csvUsername String? @db.VarChar(255)
/// Optional HTTP auth password for CSV endpoint
csvPassword String? @db.VarChar(255)
/// Company-specific dashboard configuration (theme, layout, etc.)
dashboardOpts Json?
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
/// Maximum number of users allowed for this company
maxUsers Int @default(10)
companyAiModels CompanyAIModel[]
sessions Session[]
imports SessionImport[]
users User[] @relation("CompanyUsers")
@@index([name])
@@index([status])
}
/// *
/// * USER (unified authentication accounts)
/// * All users: platform admins (companyId=null) and company users (companyId set)
/// * Authentication handled by Neon Auth, this table stores authorization data
model User {
id String @id @default(uuid())
/// User email address, must be unique (synced from Neon Auth)
email String @unique @db.VarChar(255)
/// User permission level (includes both platform and company roles)
role UserRole @default(USER)
/// Foreign key to Company - null for platform users, set for company users
companyId String?
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
/// Display name for the user
name String? @db.VarChar(255)
/// When this user was invited
invitedAt DateTime? @db.Timestamptz(6)
/// Email of the user who invited this user (for audit trail)
invitedBy String? @db.VarChar(255)
company Company? @relation("CompanyUsers", fields: [companyId], references: [id], onDelete: Cascade)
@@index([companyId])
@@index([email])
@@index([role])
}
/// *
/// * SESSION (processed conversation data)
/// * Normalized session data derived from raw CSV imports
/// * Contains AI-enhanced data like sentiment analysis and categorization
/// * 1:1 relationship with SessionImport via importId
model Session {
id String @id @default(uuid())
/// Foreign key to Company for data isolation
companyId String
/// Optional 1:1 link to source SessionImport record
importId String? @unique
/// Session timing and basic data
/// When the conversation started
startTime DateTime @db.Timestamptz(6)
/// When the conversation ended
endTime DateTime @db.Timestamptz(6)
/// Client IP address (IPv4/IPv6)
ipAddress String? @db.Inet
/// ISO 3166-1 alpha-3 country code
country String? @db.VarChar(3)
/// URL to external transcript source
fullTranscriptUrl String?
/// Average response time in seconds
avgResponseTime Float? @db.Real
/// First message in the conversation
initialMsg String?
/// ISO 639 language code
language String? @db.VarChar(10)
/// Total number of messages in session
messagesSent Int?
/// AI-enhanced analysis fields
/// AI-determined overall sentiment
sentiment SentimentCategory?
/// Whether session was escalated to human
escalated Boolean?
/// Whether session was forwarded to HR
forwardedHr Boolean?
/// AI-determined conversation category
category SessionCategory?
/// AI-generated session summary
summary String?
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
aiProcessingRequests AIProcessingRequest[]
messages Message[]
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
import SessionImport? @relation("ImportToSession", fields: [importId], references: [id])
processingStatus SessionProcessingStatus[]
sessionQuestions SessionQuestion[]
@@index([companyId, startTime])
@@index([companyId, sentiment])
@@index([companyId, category])
@@index([companyId, escalated])
@@index([companyId, forwardedHr])
@@index([companyId, language])
@@index([companyId, messagesSent])
@@index([companyId, avgResponseTime])
}
/// *
/// * 2. Raw CSV row (pure data storage) ----------
model SessionImport {
id String @id @default(uuid())
companyId String
externalSessionId String
startTimeRaw String @db.VarChar(255)
endTimeRaw String @db.VarChar(255)
ipAddress String? @db.VarChar(45)
countryCode String? @db.VarChar(3)
language String? @db.VarChar(10)
messagesSent Int?
sentimentRaw String? @db.VarChar(50)
escalatedRaw String? @db.VarChar(50)
forwardedHrRaw String? @db.VarChar(50)
fullTranscriptUrl String?
avgResponseTimeSeconds Float? @db.Real
tokens Int?
tokensEur Float? @db.Real
category String? @db.VarChar(255)
initialMessage String?
rawTranscriptContent String?
createdAt DateTime @default(now()) @db.Timestamptz(6)
session Session? @relation("ImportToSession")
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
@@unique([companyId, externalSessionId])
@@index([companyId])
@@index([companyId, createdAt])
}
/// *
/// * MESSAGE (individual lines)
model Message {
id String @id @default(uuid())
sessionId String
timestamp DateTime? @db.Timestamptz(6)
role String @db.VarChar(50)
content String
order Int
createdAt DateTime @default(now()) @db.Timestamptz(6)
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@unique([sessionId, order])
@@index([sessionId, order])
@@index([sessionId, timestamp])
@@index([sessionId, role])
}
/// *
/// * UNIFIED PROCESSING STATUS TRACKING
model SessionProcessingStatus {
id String @id @default(uuid())
sessionId String
stage ProcessingStage
status ProcessingStatus @default(PENDING)
startedAt DateTime? @db.Timestamptz(6)
completedAt DateTime? @db.Timestamptz(6)
errorMessage String?
retryCount Int @default(0)
metadata Json?
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@unique([sessionId, stage])
@@index([stage, status])
@@index([sessionId])
@@index([status, startedAt])
}
/// *
/// * QUESTION MANAGEMENT (separate from Session for better analytics)
model Question {
id String @id @default(uuid())
content String @unique
createdAt DateTime @default(now()) @db.Timestamptz(6)
sessionQuestions SessionQuestion[]
}
model SessionQuestion {
id String @id @default(uuid())
sessionId String
questionId String
order Int
createdAt DateTime @default(now()) @db.Timestamptz(6)
question Question @relation(fields: [questionId], references: [id])
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@unique([sessionId, questionId])
@@unique([sessionId, order])
@@index([sessionId])
@@index([questionId])
}
/// *
/// * AI PROCESSING COST TRACKING
model AIProcessingRequest {
id String @id @default(uuid())
sessionId String
openaiRequestId String? @db.VarChar(255)
model String @db.VarChar(100)
serviceTier String? @db.VarChar(50)
systemFingerprint String? @db.VarChar(255)
promptTokens Int
completionTokens Int
totalTokens Int
cachedTokens Int?
audioTokensPrompt Int?
reasoningTokens Int?
audioTokensCompletion Int?
acceptedPredictionTokens Int?
rejectedPredictionTokens Int?
promptTokenCost Float @db.Real
completionTokenCost Float @db.Real
totalCostEur Float @db.Real
processingType String @db.VarChar(100)
success Boolean
errorMessage String?
requestedAt DateTime @default(now()) @db.Timestamptz(6)
completedAt DateTime? @db.Timestamptz(6)
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@index([sessionId])
@@index([sessionId, requestedAt])
@@index([requestedAt])
@@index([model])
@@index([success, requestedAt])
}
/// *
/// * AI Model definitions (without pricing)
model AIModel {
id String @id @default(uuid())
name String @unique @db.VarChar(100)
provider String @db.VarChar(50)
maxTokens Int?
isActive Boolean @default(true)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
pricing AIModelPricing[]
companyModels CompanyAIModel[]
@@index([provider, isActive])
@@index([name])
}
/// *
/// * Time-based pricing for AI models
model AIModelPricing {
id String @id @default(uuid())
aiModelId String
promptTokenCost Float @db.Real
completionTokenCost Float @db.Real
effectiveFrom DateTime @db.Timestamptz(6)
effectiveUntil DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
@@index([aiModelId, effectiveFrom])
@@index([effectiveFrom, effectiveUntil])
}
/// *
/// * Company-specific AI model assignments
model CompanyAIModel {
id String @id @default(uuid())
companyId String
aiModelId String
isDefault Boolean @default(false)
createdAt DateTime @default(now()) @db.Timestamptz(6)
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
@@unique([companyId, aiModelId])
@@index([companyId, isDefault])
}
/// Unified user roles for both platform and company users
enum UserRole {
/// Platform: Full platform access, can create/suspend companies
PLATFORM_SUPER_ADMIN
/// Platform: Platform administration, company management
PLATFORM_ADMIN
/// Platform: Customer support access, read-only company access
PLATFORM_SUPPORT
/// Company: Full access to company data and settings
ADMIN
/// Company: Standard access to view and interact with data
USER
}
/// Company operational status
enum CompanyStatus {
/// Company is operational and can access all features
ACTIVE
/// Company access is temporarily disabled
SUSPENDED
/// Company is in trial period with potential limitations
TRIAL
/// Company is archived and data is read-only
ARCHIVED
}
/// AI-determined sentiment categories for sessions
enum SentimentCategory {
/// Customer expressed satisfaction or positive emotions
POSITIVE
/// Neutral tone or mixed emotions
NEUTRAL
/// Customer expressed frustration or negative emotions
NEGATIVE
}
/// AI-determined conversation categories based on content analysis
enum SessionCategory {
/// Questions about work schedules and hours
SCHEDULE_HOURS
/// Vacation requests and leave policies
LEAVE_VACATION
/// Sick leave and recovery-related discussions
SICK_LEAVE_RECOVERY
/// Salary, benefits, and compensation questions
SALARY_COMPENSATION
/// Contract terms and working hours
CONTRACT_HOURS
/// New employee onboarding processes
ONBOARDING
/// Employee departure and offboarding
OFFBOARDING
/// Equipment, uniforms, and access cards
WORKWEAR_STAFF_PASS
/// Team directory and contact information
TEAM_CONTACTS
/// Personal HR matters and private concerns
PERSONAL_QUESTIONS
/// System access and login issues
ACCESS_LOGIN
/// Social events and company culture
SOCIAL_QUESTIONS
/// Conversations that don't fit other categories
UNRECOGNIZED_OTHER
}
/// Processing pipeline stages for session data transformation
enum ProcessingStage {
/// Initial import of raw CSV data into SessionImport
CSV_IMPORT
/// Fetching transcript content from external URLs
TRANSCRIPT_FETCH
/// Converting SessionImport to normalized Session
SESSION_CREATION
/// AI processing for sentiment, categorization, summaries
AI_ANALYSIS
/// Extracting questions from conversation content
QUESTION_EXTRACTION
}
/// Status of each processing stage
enum ProcessingStatus {
/// Stage is queued for processing
PENDING
/// Stage is currently being processed
IN_PROGRESS
/// Stage completed successfully
COMPLETED
/// Stage failed with errors
FAILED
/// Stage was intentionally skipped
SKIPPED
}