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? /** 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? /** Optional Gmail auto-reply HTML theming (colors, font, logo URL). Merged with defaults server-side. */ gmailReplyTheme 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 /** 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 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? /** 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 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]) }