05aad75272
Multi-tenant AI help desk SaaS for the firearms industry. Full monorepo: API (Express/Prisma), Worker (BullMQ), Frontend (React/Vite/Tailwind). PostgreSQL 16 + pgvector, Redis 7, JWT auth, RLS tenant isolation. Dark Armory theme with tactical branding throughout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
344 lines
13 KiB
Plaintext
344 lines
13 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ============================================================
|
|
// Plans
|
|
// ============================================================
|
|
model Plan {
|
|
id String @id @default(uuid())
|
|
name String
|
|
slug String @unique
|
|
maxUsers Int @default(3)
|
|
maxEmailAccounts Int @default(1)
|
|
maxTicketsPerMonth Int @default(500)
|
|
aiDraftsEnabled Boolean @default(false)
|
|
knowledgeBaseEnabled Boolean @default(false)
|
|
priceMonthly Decimal @default(0) @db.Decimal(10, 2)
|
|
priceYearly Decimal @default(0) @db.Decimal(10, 2)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
tenants Tenant[]
|
|
|
|
@@map("plans")
|
|
}
|
|
|
|
// ============================================================
|
|
// Tenants (Multi-tenant root)
|
|
// ============================================================
|
|
model Tenant {
|
|
id String @id @default(uuid())
|
|
name String
|
|
slug String @unique
|
|
planId String @map("plan_id")
|
|
settings Json @default("{}")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
plan Plan @relation(fields: [planId], references: [id])
|
|
users User[]
|
|
emailAccounts EmailAccount[]
|
|
tickets Ticket[]
|
|
messages Message[]
|
|
aiDrafts AiDraft[]
|
|
knowledgeBase KnowledgeBaseEntry[]
|
|
auditLogs AuditLog[]
|
|
customerProfiles CustomerProfile[]
|
|
cannedResponses CannedResponse[]
|
|
notificationPreferences NotificationPreference[]
|
|
|
|
@@map("tenants")
|
|
}
|
|
|
|
// ============================================================
|
|
// Users
|
|
// ============================================================
|
|
model User {
|
|
id String @id @default(uuid())
|
|
tenantId String @map("tenant_id")
|
|
email String
|
|
passwordHash String @map("password_hash")
|
|
name String
|
|
role String @default("agent")
|
|
avatarUrl String? @map("avatar_url")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
lastLoginAt DateTime? @map("last_login_at")
|
|
refreshToken String? @map("refresh_token")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
assignedTickets Ticket[] @relation("TicketAssignee")
|
|
auditLogs AuditLog[]
|
|
notificationPreference NotificationPreference?
|
|
createdCannedResponses CannedResponse[]
|
|
|
|
@@unique([tenantId, email])
|
|
@@index([tenantId])
|
|
@@map("users")
|
|
}
|
|
|
|
// ============================================================
|
|
// Email Accounts
|
|
// ============================================================
|
|
model EmailAccount {
|
|
id String @id @default(uuid())
|
|
tenantId String @map("tenant_id")
|
|
name String
|
|
emailAddress String @map("email_address")
|
|
imapHost String @map("imap_host")
|
|
imapPort Int @default(993) @map("imap_port")
|
|
imapUser String @map("imap_user")
|
|
imapPassword String @map("imap_password")
|
|
imapTls Boolean @default(true) @map("imap_tls")
|
|
smtpHost String @map("smtp_host")
|
|
smtpPort Int @default(587) @map("smtp_port")
|
|
smtpUser String @map("smtp_user")
|
|
smtpPassword String @map("smtp_password")
|
|
smtpTls Boolean @default(true) @map("smtp_tls")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
lastPollAt DateTime? @map("last_poll_at")
|
|
lastError String? @map("last_error")
|
|
pollIntervalSeconds Int @default(60) @map("poll_interval_seconds")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
tickets Ticket[]
|
|
|
|
@@unique([tenantId, emailAddress])
|
|
@@index([tenantId])
|
|
@@map("email_accounts")
|
|
}
|
|
|
|
// ============================================================
|
|
// Tickets
|
|
// ============================================================
|
|
model Ticket {
|
|
id String @id @default(uuid())
|
|
tenantId String @map("tenant_id")
|
|
ticketNumber String @map("ticket_number")
|
|
emailAccountId String @map("email_account_id")
|
|
subject String
|
|
status String @default("incoming")
|
|
priority String @default("medium")
|
|
assigneeId String? @map("assignee_id")
|
|
customerEmail String @map("customer_email")
|
|
customerName String? @map("customer_name")
|
|
customerProfileId String? @map("customer_profile_id")
|
|
tags String[] @default([])
|
|
messageCount Int @default(1) @map("message_count")
|
|
lastMessageAt DateTime @default(now()) @map("last_message_at")
|
|
resolvedAt DateTime? @map("resolved_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
emailAccount EmailAccount @relation(fields: [emailAccountId], references: [id])
|
|
assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id])
|
|
customerProfile CustomerProfile? @relation(fields: [customerProfileId], references: [id])
|
|
messages Message[]
|
|
aiDrafts AiDraft[]
|
|
|
|
@@unique([tenantId, ticketNumber])
|
|
@@index([tenantId, status])
|
|
@@index([tenantId, assigneeId])
|
|
@@index([tenantId, customerEmail])
|
|
@@index([tenantId, createdAt])
|
|
@@map("tickets")
|
|
}
|
|
|
|
// ============================================================
|
|
// Messages
|
|
// ============================================================
|
|
model Message {
|
|
id String @id @default(uuid())
|
|
ticketId String @map("ticket_id")
|
|
tenantId String @map("tenant_id")
|
|
direction String // 'inbound' | 'outbound'
|
|
fromEmail String @map("from_email")
|
|
fromName String? @map("from_name")
|
|
toEmail String @map("to_email")
|
|
subject String
|
|
bodyText String @map("body_text")
|
|
bodyHtml String? @map("body_html")
|
|
messageId String? @map("message_id")
|
|
inReplyTo String? @map("in_reply_to")
|
|
references String?
|
|
headers Json?
|
|
sentAt DateTime @default(now()) @map("sent_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
|
attachments Attachment[]
|
|
aiDrafts AiDraft[]
|
|
|
|
@@index([ticketId])
|
|
@@index([tenantId])
|
|
@@index([messageId])
|
|
@@map("messages")
|
|
}
|
|
|
|
// ============================================================
|
|
// Attachments
|
|
// ============================================================
|
|
model Attachment {
|
|
id String @id @default(uuid())
|
|
messageId String @map("message_id")
|
|
filename String
|
|
contentType String @map("content_type")
|
|
size Int
|
|
storageKey String @map("storage_key")
|
|
|
|
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("attachments")
|
|
}
|
|
|
|
// ============================================================
|
|
// AI Drafts
|
|
// ============================================================
|
|
model AiDraft {
|
|
id String @id @default(uuid())
|
|
ticketId String @map("ticket_id")
|
|
tenantId String @map("tenant_id")
|
|
messageId String @map("message_id")
|
|
draftBody String @map("draft_body")
|
|
confidence Float @default(0)
|
|
model String @default("gpt-4o")
|
|
tokensUsed Int @default(0) @map("tokens_used")
|
|
status String @default("pending")
|
|
editedBody String? @map("edited_body")
|
|
reviewedBy String? @map("reviewed_by")
|
|
reviewedAt DateTime? @map("reviewed_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
|
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([ticketId])
|
|
@@index([tenantId])
|
|
@@map("ai_drafts")
|
|
}
|
|
|
|
// ============================================================
|
|
// Knowledge Base
|
|
// ============================================================
|
|
model KnowledgeBaseEntry {
|
|
id String @id @default(uuid())
|
|
tenantId String @map("tenant_id")
|
|
title String
|
|
content String
|
|
category String @default("general")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([tenantId])
|
|
@@map("knowledge_base")
|
|
}
|
|
|
|
// ============================================================
|
|
// Audit Log
|
|
// ============================================================
|
|
model AuditLog {
|
|
id String @id @default(uuid())
|
|
tenantId String @map("tenant_id")
|
|
userId String? @map("user_id")
|
|
action String
|
|
entity String
|
|
entityId String? @map("entity_id")
|
|
details Json?
|
|
ipAddress String? @map("ip_address")
|
|
userAgent String? @map("user_agent")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
user User? @relation(fields: [userId], references: [id])
|
|
|
|
@@index([tenantId, createdAt])
|
|
@@index([tenantId, entity])
|
|
@@map("audit_log")
|
|
}
|
|
|
|
// ============================================================
|
|
// Customer Profiles
|
|
// ============================================================
|
|
model CustomerProfile {
|
|
id String @id @default(uuid())
|
|
tenantId String @map("tenant_id")
|
|
email String
|
|
name String?
|
|
phone String?
|
|
company String?
|
|
notes String?
|
|
tags String[] @default([])
|
|
ticketCount Int @default(0) @map("ticket_count")
|
|
firstContactAt DateTime @default(now()) @map("first_contact_at")
|
|
lastContactAt DateTime @default(now()) @map("last_contact_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
tickets Ticket[]
|
|
|
|
@@unique([tenantId, email])
|
|
@@index([tenantId])
|
|
@@map("customer_profiles")
|
|
}
|
|
|
|
// ============================================================
|
|
// Canned Responses
|
|
// ============================================================
|
|
model CannedResponse {
|
|
id String @id @default(uuid())
|
|
tenantId String @map("tenant_id")
|
|
title String
|
|
body String
|
|
category String @default("general")
|
|
shortcut String?
|
|
createdBy String @map("created_by")
|
|
isShared Boolean @default(true) @map("is_shared")
|
|
usageCount Int @default(0) @map("usage_count")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
creator User @relation(fields: [createdBy], references: [id])
|
|
|
|
@@index([tenantId])
|
|
@@map("canned_responses")
|
|
}
|
|
|
|
// ============================================================
|
|
// Notification Preferences
|
|
// ============================================================
|
|
model NotificationPreference {
|
|
id String @id @default(uuid())
|
|
userId String @unique @map("user_id")
|
|
tenantId String @map("tenant_id")
|
|
newTicket Boolean @default(true) @map("new_ticket")
|
|
ticketAssigned Boolean @default(true) @map("ticket_assigned")
|
|
ticketReply Boolean @default(true) @map("ticket_reply")
|
|
aiDraftReady Boolean @default(true) @map("ai_draft_ready")
|
|
dailyDigest Boolean @default(false) @map("daily_digest")
|
|
emailNotifications Boolean @default(true) @map("email_notifications")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([tenantId])
|
|
@@map("notification_preferences")
|
|
}
|