JUNOHUB-test/prisma/schema.prisma

662 lines
18 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
enum UserRole {
VENDOR
SUPPLIER
ADMIN
}
enum ChatMessageKind {
TEXT
PRODUCT
ORDER
}
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
// Customer-facing storefront widget (floating chat) enablement.
storefrontChatEnabled Boolean @default(false)
storefrontChatName String?
storefrontChatTagline String?
storefrontChatBrandColor String?
storefrontChatFontFamily String?
storefrontChatLogoUrl String?
/** Primary knowledge source: structured questionnaire vs uploaded documents (mutually exclusive). */
knowledgeBaseMode KnowledgeBaseMode @default(UNSET)
/** Saved structured KB answers (version, per-field text, misc). Embedded as STRUCTURED chunks. */
knowledgeTemplate Json?
// Relations
connections Connection[]
cachedProducts CachedProduct[]
cachedOrders CachedOrder[]
cachedCustomers CachedCustomer[]
storefrontChatConversations StorefrontChatConversation[]
syncMetadata SyncMetadata[]
invitations Invitation[]
productSyncs ProductSync[]
supplierProductOffers SupplierProductOffer[]
knowledgeDocuments KnowledgeDocument[]
faqItems FaqItem[]
knowledgeChunks KnowledgeChunk[]
emailLogs EmailLog[]
flaggedEmails FlaggedEmail[]
gmailInboundAutoReplies GmailInboundAutoReply[]
templates Template[]
chatMessages ChatMessage[]
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?
pendingVendorSync Boolean @default(false)
lastSupplierUpdateAt DateTime?
lastSupplierUpdatedBy String?
lastShopifySyncAt DateTime?
lastShopifySyncError 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?
/** Shopify REST `default_address` snapshot for vendor UI (city, country, etc.). */
defaultAddress Json?
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
STRUCTURED
}
enum KnowledgeBaseMode {
UNSET
STRUCTURED
DOCUMENTS
}
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
/** Vendor store this thread belongs to (B2B JUNO CHAT). Null = legacy messages before per-store threading. */
storeId String?
kind ChatMessageKind @default(TEXT)
content String @map("encryptedContent") @db.Text
attachment Json?
sender User @relation("SentMessages", fields: [senderId], references: [id])
receiver User @relation("ReceivedMessages", fields: [receiverId], references: [id])
store Store? @relation(fields: [storeId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@index([senderId, receiverId])
@@index([receiverId, senderId])
@@index([senderId, receiverId, storeId])
}
enum StorefrontChatSenderType {
CUSTOMER
AI
}
model StorefrontChatConversation {
id String @id @default(cuid())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
visitorId String
customerEmail String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages StorefrontChatMessage[]
@@unique([storeId, visitorId])
@@index([storeId])
}
model StorefrontChatMessage {
id String @id @default(cuid())
conversationId String
conversation StorefrontChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
senderType StorefrontChatSenderType
content String @db.Text
createdAt DateTime @default(now())
@@index([conversationId, createdAt])
}
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])
}