587 lines
16 KiB
Plaintext
587 lines
16 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum UserRole {
|
|
VENDOR
|
|
SUPPLIER
|
|
ADMIN
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String? @unique
|
|
emailVerified DateTime?
|
|
image String?
|
|
password String?
|
|
role UserRole @default(VENDOR)
|
|
|
|
activeStoreId String? // Track which store is currently active
|
|
|
|
// MFA (Multi-Factor Authentication)
|
|
mfaEnabled Boolean @default(false)
|
|
mfaSecret String? // Encrypted TOTP secret
|
|
|
|
// Relations
|
|
sentMessages ChatMessage[] @relation("SentMessages")
|
|
receivedMessages ChatMessage[] @relation("ReceivedMessages")
|
|
accounts Account[]
|
|
sessions Session[]
|
|
stores Store[] // Changed from singular to support multiple stores
|
|
supplierProfile SupplierProfile?
|
|
notifications Notification[]
|
|
gmailIntegration GmailIntegration?
|
|
emailLogs EmailLog[]
|
|
gmailInboundAutoReplies GmailInboundAutoReply[]
|
|
pushSubscriptions PushSubscription[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
// Auth.js / NextAuth Prisma adapter models (required for OAuth e.g. Google)
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String? @db.Text
|
|
access_token String? @db.Text
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String? @db.Text
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
}
|
|
|
|
model Store {
|
|
id String @id @default(cuid())
|
|
userId String // Removed @unique to allow multiple stores per user
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
businessName String
|
|
email String
|
|
timezone String
|
|
country String
|
|
|
|
// Shopify Credentials
|
|
shopifyStoreName String
|
|
shopifyDomain String
|
|
shopifyApiKey String
|
|
shopifyApiSecret String
|
|
shopifyAccessToken String
|
|
|
|
onboardingComplete Boolean @default(false)
|
|
isActive Boolean @default(true) // Track if this is the active store
|
|
|
|
// Relations
|
|
connections Connection[]
|
|
cachedProducts CachedProduct[]
|
|
cachedOrders CachedOrder[]
|
|
cachedCustomers CachedCustomer[]
|
|
syncMetadata SyncMetadata[]
|
|
invitations Invitation[]
|
|
productSyncs ProductSync[]
|
|
supplierProductOffers SupplierProductOffer[]
|
|
knowledgeDocuments KnowledgeDocument[]
|
|
faqItems FaqItem[]
|
|
knowledgeChunks KnowledgeChunk[]
|
|
emailLogs EmailLog[]
|
|
flaggedEmails FlaggedEmail[]
|
|
gmailInboundAutoReplies GmailInboundAutoReply[]
|
|
templates Template[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId]) // Add index for performance
|
|
}
|
|
|
|
model CachedProduct {
|
|
id String @id @default(cuid())
|
|
shopifyProductId String
|
|
storeId String
|
|
title String
|
|
vendor String?
|
|
productType String?
|
|
status String?
|
|
variants Json // Store variants as JSON
|
|
images Json // Store images as JSON
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
lastSyncedAt DateTime @default(now())
|
|
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([shopifyProductId, storeId])
|
|
@@index([storeId])
|
|
}
|
|
|
|
model CachedOrder {
|
|
id String @id @default(cuid())
|
|
shopifyOrderId String
|
|
storeId String
|
|
orderNumber String
|
|
email String?
|
|
totalPrice String
|
|
currency String?
|
|
financialStatus String?
|
|
fulfillmentStatus String?
|
|
trackingNumber String?
|
|
trackingCompany String?
|
|
trackingUrl String?
|
|
lineItems Json // Store line items as JSON
|
|
customer Json? // Store customer info as JSON
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
lastSyncedAt DateTime @default(now())
|
|
shopifyCreatedAt DateTime?
|
|
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([shopifyOrderId, storeId])
|
|
@@index([storeId])
|
|
@@index([shopifyCreatedAt])
|
|
}
|
|
|
|
model CachedCustomer {
|
|
id String @id @default(cuid())
|
|
shopifyCustomerId String
|
|
storeId String
|
|
email String?
|
|
firstName String?
|
|
lastName String?
|
|
ordersCount Int @default(0)
|
|
totalSpent String?
|
|
phone String?
|
|
tags String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
lastSyncedAt DateTime @default(now())
|
|
shopifyCreatedAt DateTime?
|
|
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([shopifyCustomerId, storeId])
|
|
@@index([storeId])
|
|
}
|
|
|
|
model SyncMetadata {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
resourceType String // "products", "orders", "customers"
|
|
lastSyncAt DateTime @default(now())
|
|
syncStatus String @default("success") // "success", "failed", "in_progress"
|
|
recordCount Int @default(0)
|
|
errorMessage String?
|
|
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([storeId, resourceType])
|
|
@@index([storeId])
|
|
}
|
|
|
|
model SupplierProfile {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
companyName String
|
|
description String?
|
|
|
|
// Relations
|
|
connections Connection[]
|
|
products Product[]
|
|
invitations Invitation[]
|
|
productSyncs ProductSync[]
|
|
supplierProductOffers SupplierProductOffer[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Connection {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
supplierId String
|
|
supplier SupplierProfile @relation(fields: [supplierId], references: [id])
|
|
|
|
status ConnectionStatus @default(PENDING)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([storeId, supplierId])
|
|
supplierProductOffers SupplierProductOffer[]
|
|
}
|
|
|
|
model Invitation {
|
|
id String @id @default(cuid())
|
|
token String @unique
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
supplierId String
|
|
supplier SupplierProfile @relation(fields: [supplierId], references: [id])
|
|
|
|
email String // Supplier email to send invitation to
|
|
expiresAt DateTime
|
|
used Boolean @default(false)
|
|
|
|
createdAt DateTime @default(now())
|
|
usedAt DateTime?
|
|
|
|
@@index([token])
|
|
@@index([supplierId])
|
|
@@index([storeId])
|
|
}
|
|
|
|
enum ConnectionStatus {
|
|
PENDING
|
|
CONNECTED
|
|
REJECTED
|
|
}
|
|
|
|
model Product {
|
|
id String @id @default(cuid())
|
|
supplierId String
|
|
supplier SupplierProfile @relation(fields: [supplierId], references: [id])
|
|
|
|
title String
|
|
description String?
|
|
price Float
|
|
sku String?
|
|
imageUrl String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
offers SupplierProductOffer[]
|
|
}
|
|
|
|
model SupplierProductOffer {
|
|
id String @id @default(cuid())
|
|
productId String
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
connectionId String
|
|
connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
|
|
supplierId String
|
|
supplier SupplierProfile @relation(fields: [supplierId], references: [id], onDelete: Cascade)
|
|
|
|
price Float
|
|
quantity Int
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([productId, storeId])
|
|
@@index([storeId])
|
|
@@index([supplierId])
|
|
@@index([connectionId])
|
|
}
|
|
|
|
enum ProductSyncStatus {
|
|
PENDING
|
|
ACCEPTED
|
|
REJECTED
|
|
}
|
|
|
|
model ProductSync {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id])
|
|
supplierId String
|
|
supplier SupplierProfile @relation(fields: [supplierId], references: [id])
|
|
|
|
shopifyProductId String // Shopify product ID from vendor's store
|
|
shopifyProductTitle String
|
|
shopifyProductData Json? // Store full product data for reference
|
|
|
|
status ProductSyncStatus @default(PENDING)
|
|
|
|
// Metadata
|
|
requestedBy String // userId of vendor who requested
|
|
acceptedAt DateTime?
|
|
rejectedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([storeId, supplierId, shopifyProductId])
|
|
@@index([storeId])
|
|
@@index([supplierId])
|
|
@@index([status])
|
|
@@index([shopifyProductId])
|
|
}
|
|
|
|
model GmailIntegration {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
accessToken String @db.Text
|
|
refreshToken String? @db.Text
|
|
expiresAt DateTime
|
|
|
|
email String // Gmail email address
|
|
scope String // OAuth scopes granted
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
enum NotificationType {
|
|
INVITATION_ACCEPTED
|
|
INVITATION_RECEIVED
|
|
CONNECTION_STATUS_CHANGED
|
|
ORDER_RECEIVED
|
|
PRODUCT_UPDATED
|
|
PRODUCT_SYNC_REQUESTED
|
|
PRODUCT_SYNC_ACCEPTED
|
|
PRODUCT_SYNC_REJECTED
|
|
}
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
type NotificationType
|
|
title String
|
|
message String
|
|
read Boolean @default(false)
|
|
readAt DateTime?
|
|
|
|
// Optional metadata for linking to related entities
|
|
metadata Json? // Store related IDs (connectionId, invitationId, etc.)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([userId, read])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
/** One row per inbound Gmail message id that received an auto-reply (idempotency). */
|
|
model GmailInboundAutoReply {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
gmailMessageId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([userId, gmailMessageId])
|
|
@@index([storeId, createdAt])
|
|
}
|
|
|
|
model EmailLog {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
storeId String?
|
|
store Store? @relation(fields: [storeId], references: [id])
|
|
|
|
to String
|
|
subject String
|
|
bodyPreview String?
|
|
|
|
// Whether this email was triggered automatically by the system
|
|
isAutomated Boolean @default(false)
|
|
trigger String? // e.g. "GMAIL_AUTO_REPLY", "TICKET_CREATED"
|
|
|
|
status String // "SENT" | "FAILED"
|
|
providerMessageId String?
|
|
errorMessage String?
|
|
|
|
sentAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([storeId, sentAt])
|
|
@@index([userId, sentAt])
|
|
}
|
|
|
|
enum FlaggedEmailPriority {
|
|
LOW
|
|
MEDIUM
|
|
HIGH
|
|
}
|
|
|
|
model FlaggedEmail {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
subject String @db.Text
|
|
from String // Sender email
|
|
reason String @db.Text
|
|
priority FlaggedEmailPriority @default(MEDIUM)
|
|
messageId String? // Gmail message id for future sync
|
|
flaggedAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([storeId])
|
|
@@index([storeId, flaggedAt])
|
|
}
|
|
|
|
enum KnowledgeDocumentStatus {
|
|
UPLOADED
|
|
PROCESSING
|
|
READY
|
|
FAILED
|
|
}
|
|
|
|
enum KnowledgeChunkSourceType {
|
|
DOCUMENT
|
|
FAQ
|
|
}
|
|
|
|
model KnowledgeDocument {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
title String
|
|
fileName String
|
|
mimeType String
|
|
sizeBytes Int
|
|
status KnowledgeDocumentStatus @default(UPLOADED)
|
|
errorMessage String?
|
|
|
|
// Where the raw file is stored (local path or object storage key)
|
|
storagePath String
|
|
|
|
// Relations
|
|
chunks KnowledgeChunk[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([storeId])
|
|
}
|
|
|
|
model FaqItem {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
question String
|
|
answer String
|
|
tags Json?
|
|
|
|
// Relations
|
|
chunks KnowledgeChunk[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([storeId])
|
|
}
|
|
|
|
model KnowledgeChunk {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
documentId String?
|
|
document KnowledgeDocument? @relation(fields: [documentId], references: [id])
|
|
|
|
faqItemId String?
|
|
faqItem FaqItem? @relation(fields: [faqItemId], references: [id])
|
|
|
|
sourceType KnowledgeChunkSourceType
|
|
content String
|
|
metadata Json?
|
|
|
|
// pgvector-backed column; accessed via raw SQL for similarity search
|
|
embedding Unsupported("vector")
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([storeId])
|
|
@@index([documentId])
|
|
@@index([faqItemId])
|
|
}
|
|
|
|
model Template {
|
|
id String @id @default(cuid())
|
|
storeId String
|
|
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
|
|
|
name String
|
|
intent String
|
|
mood String
|
|
description String?
|
|
|
|
body String // Template body (HTML or plain text with {{variables}})
|
|
variables Json? // Optional metadata describing expected variables
|
|
isHtml Boolean @default(true)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([storeId])
|
|
}
|
|
|
|
model ChatMessage {
|
|
id String @id @default(cuid())
|
|
senderId String
|
|
receiverId String
|
|
content String @map("encryptedContent") @db.Text
|
|
|
|
sender User @relation("SentMessages", fields: [senderId], references: [id])
|
|
receiver User @relation("ReceivedMessages", fields: [receiverId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([senderId, receiverId])
|
|
@@index([receiverId, senderId])
|
|
}
|
|
model PushSubscription {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
endpoint String @unique
|
|
p256dh String // Public key for encryption
|
|
auth String // Authentication secret
|
|
|
|
userAgent String? // Optional: device/browser info
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
}
|