Add strict linked-product upsell controls, improve retrieval consistency, persist full storefront escalation transcripts, and upgrade ticket APIs/UI for complete thread visibility and safer human handoff workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
869 lines
24 KiB
Plaintext
869 lines
24 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)
|
||
/**
|
||
* After OTP verify, widget session length (minutes). Clamped server-side (e.g. 5–10080). Default 60.
|
||
*/
|
||
storefrontOtpSessionMinutes Int @default(60)
|
||
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?
|
||
|
||
/**
|
||
* When true, L1 may use DB-backed category/product metafield tools for customer answers.
|
||
*/
|
||
categoryMetadetailsEnabled Boolean @default(false)
|
||
/**
|
||
* Per-module AI enablement, e.g. { "FAQ": true, "ORDER_STATUS": true }.
|
||
*/
|
||
aiModules Json?
|
||
/**
|
||
* FAQ-style policy text per L1 module for embeddings, e.g. { "ORDER_STATUS": "..." }.
|
||
*/
|
||
modulePolicies Json?
|
||
/**
|
||
* Vendor Type-1 answers keyed by shared_field_id (sf_*); one value per key store-wide (§4.2).
|
||
*/
|
||
sharedFieldAnswers Json?
|
||
/**
|
||
* Vendor-created knowledge modules. Each enabled module is embedded into STRUCTURED chunks.
|
||
*/
|
||
customAiModules Json?
|
||
|
||
/**
|
||
* Optional Gmail auto-reply HTML theming (colors, font, logo URL). Merged with defaults server-side.
|
||
*/
|
||
gmailReplyTheme Json?
|
||
|
||
/**
|
||
* When set, supplier-reported stock (ProductSync snapshot) below this value can trigger
|
||
* B2B low-stock chat to linked suppliers. Null = off for this signal.
|
||
*/
|
||
lowStockThreshold Int?
|
||
/**
|
||
* When set, total Shopify variant inventory (sum of inventory_quantity) below this value
|
||
* triggers an in-app notification to the vendor (restock / Load stock). Null = off.
|
||
*/
|
||
lowStockShopifyThreshold Int?
|
||
|
||
// 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[]
|
||
aiAnalyticsEvents AiAnalyticsEvent[]
|
||
lowStockAlertStates LowStockAlertState[]
|
||
lowStockShopifyVendorAlertStates LowStockShopifyVendorAlertState[]
|
||
supplierAutomationLogs SupplierAutomationLog[]
|
||
upsellProductLinksFrom UpsellProductLink[] @relation("UpsellLinksFromStore")
|
||
upsellProductLinksTo UpsellProductLink[] @relation("UpsellLinksToStore")
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([userId]) // Add index for performance
|
||
}
|
||
|
||
/**
|
||
* Dedupes low-stock B2B notifications per store + product + supplier while supplier-reported qty stays below threshold.
|
||
*/
|
||
model LowStockAlertState {
|
||
id String @id @default(cuid())
|
||
storeId String
|
||
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
||
shopifyProductId String
|
||
supplierId String
|
||
supplier SupplierProfile @relation(fields: [supplierId], references: [id], onDelete: Cascade)
|
||
lastSeenSupplierQty Int?
|
||
/**
|
||
* Set when we sent a low-stock alert; cleared when supplier qty is back >= threshold.
|
||
*/
|
||
lastNotifiedBelowAt DateTime?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([storeId, shopifyProductId, supplierId])
|
||
@@index([storeId])
|
||
@@index([supplierId])
|
||
}
|
||
|
||
/**
|
||
* Dedupes Shopify low-stock vendor notifications per store + product while total stays below threshold.
|
||
*/
|
||
model LowStockShopifyVendorAlertState {
|
||
id String @id @default(cuid())
|
||
storeId String
|
||
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
||
shopifyProductId String
|
||
lastSeenShopifyTotal Int?
|
||
/**
|
||
* Set when we created a vendor notification; cleared when Shopify total is back >= threshold.
|
||
*/
|
||
lastNotifiedBelowAt DateTime?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([storeId, shopifyProductId])
|
||
@@index([storeId])
|
||
}
|
||
|
||
enum SupplierAutomationKind {
|
||
LOW_STOCK
|
||
ASK_SUPPLIER
|
||
}
|
||
|
||
model SupplierAutomationLog {
|
||
id String @id @default(cuid())
|
||
storeId String
|
||
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
||
kind SupplierAutomationKind
|
||
shopifyProductId String?
|
||
supplierUserId String?
|
||
ticketKey String?
|
||
messagePreview String @db.Text
|
||
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([storeId, kind, createdAt])
|
||
@@index([storeId, shopifyProductId, supplierUserId, createdAt])
|
||
}
|
||
|
||
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
|
||
/**
|
||
* Shopify product metafields (incl. category / taxonomy fields from admin) for automation & search.
|
||
*/
|
||
metafields 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?
|
||
/**
|
||
* When fulfillment is ON_HOLD: machine reason (see SUPPLIER_HOLD_REASON_CODES).
|
||
*/
|
||
holdReasonCode String?
|
||
holdNote String? @db.Text
|
||
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
|
||
noteAttributes 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[]
|
||
lowStockAlertStates LowStockAlertState[]
|
||
|
||
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
|
||
supplierProductOffers SupplierProductOffer[]
|
||
|
||
@@unique([storeId, supplierId])
|
||
}
|
||
|
||
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
|
||
SHOPIFY_LOW_STOCK
|
||
}
|
||
|
||
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?
|
||
metadata Json?
|
||
storefrontConversationId 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])
|
||
@@index([storeId, storefrontConversationId, sentAt])
|
||
}
|
||
|
||
model UpsellProductLink {
|
||
id String @id @default(cuid())
|
||
storeId String
|
||
store Store @relation("UpsellLinksFromStore", fields: [storeId], references: [id], onDelete: Cascade)
|
||
sourceShopifyProductId String
|
||
targetShopifyProductId String
|
||
targetStoreId String?
|
||
targetStore Store? @relation("UpsellLinksToStore", fields: [targetStoreId], references: [id], onDelete: SetNull)
|
||
notes String?
|
||
isActive Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([storeId, sourceShopifyProductId, targetShopifyProductId])
|
||
@@index([storeId, sourceShopifyProductId, isActive])
|
||
@@index([storeId, targetShopifyProductId, isActive])
|
||
}
|
||
|
||
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 @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?
|
||
|
||
/**
|
||
* Email the customer entered for OTP (asserted order email).
|
||
*/
|
||
assertedEmail String?
|
||
emailVerifiedAt DateTime?
|
||
/**
|
||
* Verified session TTL (e.g. 1h from verification).
|
||
*/
|
||
sessionExpiresAt DateTime?
|
||
/**
|
||
* After order disambiguation, pinned Shopify order id for this session.
|
||
*/
|
||
boundShopifyOrderId String?
|
||
/**
|
||
* SHA-256 hex of OTP + store-visitor salt; cleared after verify.
|
||
*/
|
||
otpHash String?
|
||
otpExpiresAt DateTime?
|
||
/**
|
||
* L1 orchestrator: phase, module, slots (JSON).
|
||
*/
|
||
l1State Json?
|
||
|
||
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
|
||
actions Json?
|
||
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([conversationId, createdAt])
|
||
}
|
||
|
||
model AiAnalyticsEvent {
|
||
id String @id @default(cuid())
|
||
storeId String
|
||
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
|
||
conversationId String?
|
||
messageId String?
|
||
eventType String
|
||
source String
|
||
productId String?
|
||
variantId String?
|
||
quantity Int?
|
||
amount Float?
|
||
currency String?
|
||
sentiment String?
|
||
resolutionStatus String?
|
||
orderId String?
|
||
attributionType String?
|
||
metadata Json?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([storeId, eventType, createdAt])
|
||
@@index([storeId, createdAt])
|
||
@@index([conversationId])
|
||
@@index([productId])
|
||
@@index([orderId])
|
||
}
|
||
|
||
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])
|
||
}
|