Phase 1: Forward Assist initial build
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>
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user