// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } model ProductBrand { id Int @id @default(autoincrement()) name String @unique products Product[] translations ProductBrandTranslation[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ProductBrandTranslation { id Int @id @default(autoincrement()) brandId Int brand ProductBrand @relation(fields: [brandId], references: [id], onDelete: Cascade) langCode String name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([brandId, langCode]) } model Category { id Int @id @default(autoincrement()) name String @unique imageUrl String? products Product[] attributes AttributeDefinition[] translations CategoryTranslation[] deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([deletedAt]) } model CategoryTranslation { id Int @id @default(autoincrement()) categoryId Int category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) langCode String name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([categoryId, langCode]) } model Product { id Int @id @default(autoincrement()) categoryId Int category Category @relation(fields: [categoryId], references: [id]) name String? description String? details String? brandId Int brand ProductBrand @relation(fields: [brandId], references: [id]) options Json? @default("[]") // Expected shape: [{ name: "Color", values: ["Red", "Blue"] }, ...] variants ProductVariant[] images ProductImage[] attributes ProductAttribute[] reviews Review[] detailSections ProductDetailSection[] translations ProductTranslation[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ProductTranslation { id Int @id @default(autoincrement()) productId Int product Product @relation(fields: [productId], references: [id], onDelete: Cascade) langCode String name String description String? details String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([productId, langCode]) } model ProductImage { id Int @id @default(autoincrement()) url String order Int @default(0) productId Int product Product @relation(fields: [productId], references: [id], onDelete: Cascade) } model ProductVariant { id Int @id @default(autoincrement()) sku String? @unique barcode String? price Float? @default(0) stock Int @default(0) option1 String @default("") // Maps to Product.options[0] option2 String @default("") // Maps to Product.options[1] option3 String @default("") // Maps to Product.options[2] productId Int product Product @relation(fields: [productId], references: [id]) bomItems BomItem[] marketplaceProducts MarketplaceProduct[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([productId, option1, option2, option3]) } model AttributeDefinition { id Int @id @default(autoincrement()) code String @unique label String description String? type String // TEXT, NUMBER, BOOLEAN, SELECT, JSON unit String? options Json? // Expected shape: [{ value: "floral", label: { th: "ดอกไม้", en: "Floral" } }] isRequired Boolean @default(false) isFilterable Boolean @default(false) sortOrder Int @default(0) categoryId Int? category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade) products ProductAttribute[] translations AttributeDefinitionTranslation[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([categoryId]) @@index([sortOrder]) } model AttributeDefinitionTranslation { id Int @id @default(autoincrement()) attributeDefinitionId Int attributeDefinition AttributeDefinition @relation(fields: [attributeDefinitionId], references: [id], onDelete: Cascade) langCode String label String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([attributeDefinitionId, langCode]) } model ProductAttribute { id Int @id @default(autoincrement()) productId Int product Product @relation(fields: [productId], references: [id], onDelete: Cascade) attributeDefinitionId Int attributeDefinition AttributeDefinition @relation(fields: [attributeDefinitionId], references: [id], onDelete: Cascade) valueText String? valueNumber Float? valueBoolean Boolean? valueJson Json? translations ProductAttributeTranslation[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([productId, attributeDefinitionId]) @@index([productId]) @@index([attributeDefinitionId]) } model ProductAttributeTranslation { id Int @id @default(autoincrement()) productAttributeId Int productAttribute ProductAttribute @relation(fields: [productAttributeId], references: [id], onDelete: Cascade) langCode String valueText String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([productAttributeId, langCode]) @@index([productAttributeId]) } model User { id Int @id @default(autoincrement()) username String @unique password String role String @default("ADMIN") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Customer { id Int @id @default(autoincrement()) lineUserId String @unique displayName String? pictureUrl String? email String? phone String? points Int @default(0) orders Order[] reviews Review[] addresses CustomerAddress[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model CustomerAddress { id Int @id @default(autoincrement()) customerId Int customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) recipientName String phone String addressLine String // บ้านเลขที่ หมู่บ้าน ถนน ซอย subdistrict String // แขวง/ตำบล district String // เขต/อำเภอ province String // จังหวัด zipcode String // รหัสไปรษณีย์ isDefault Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Coupon { id Int @id @default(autoincrement()) code String @unique type String // "PERCENTAGE", "FIXED_AMOUNT", "FREE_SHIPPING" value Float minPurchase Float @default(0) maxDiscount Float? usageLimit Int? usedCount Int @default(0) expiresAt DateTime? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Promotion { id Int @id @default(autoincrement()) name String level Int type String value Float minPurchase Float @default(0) isActive Boolean @default(true) startDate DateTime? endDate DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model StoreSettings { id Int @id @default(1) brandName String @default("HomAroma") logoUrl String? themePreset String @default("default") themeTokens Json? shippingFee Float @default(35) bankName String @default("KASIKORNBANK (KBANK)") bankAccountName String @default("HomAroma Co., Ltd.") bankAccountNumber String @default("012-3-45678-9") pointsEarnRate Float @default(1.0) facebookUrl String? footerDescription String? instagramUrl String? tiktokUrl String? newsletterTitle String? newsletterDescription String? newsletterPlaceholder String? newsletterButtonText String? footerSupportHeading Json? footerCopyrightText Json? scrollRevealEnabled Boolean @default(true) scrollRevealStyle String @default("fade-up") scrollRevealDuration Int @default(700) headerStyle String @default("single_line") translations StoreSettingsTranslation[] updatedAt DateTime @updatedAt } model StoreSettingsTranslation { id Int @id @default(autoincrement()) storeSettingsId Int storeSettings StoreSettings @relation(fields: [storeSettingsId], references: [id], onDelete: Cascade) langCode String footerDescription String? newsletterTitle String? newsletterDescription String? newsletterPlaceholder String? newsletterButtonText String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([storeSettingsId, langCode]) } model NewsletterSubscriber { id Int @id @default(autoincrement()) email String @unique isActive Boolean @default(true) createdAt DateTime @default(now()) } model Order { id Int @id @default(autoincrement()) customerName String customerEmail String customerPhone String shippingAddress String subtotal Float @default(0) promoLevel1Name String? promoLevel1Disc Float @default(0) promoLevel2Name String? promoLevel2Disc Float @default(0) couponCode String? discountAmount Float @default(0) pointsUsed Int @default(0) pointsDiscount Float @default(0) pointsEarned Int @default(0) shippingFee Float @default(35) totalAmount Float status String @default("PENDING_PAYMENT") paymentSlipUrl String? trackingNo String? customerId Int? customer Customer? @relation(fields: [customerId], references: [id]) items OrderItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model OrderItem { id Int @id @default(autoincrement()) orderId Int order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) productId Int variantId Int? quantity Int priceAtPurchase Float productName String variantName String? productImage String? } model Article { id Int @id @default(autoincrement()) slug String @unique title String coverImage String? content String isPublished Boolean @default(false) translations ArticleTranslation[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ArticleTranslation { id Int @id @default(autoincrement()) articleId Int article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) langCode String title String? content String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([articleId, langCode]) } model TiktokIncome { id String @id // Order/adjustment ID type String orderCreatedTime String? orderSettledTime String? currency String totalSettlementAmount Float totalRevenue Float totalFees Float rawData Json // Stores all other 50+ columns createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model TiktokProduct { id String @id // Product ID productName String status String gmv Float itemsSold Int orders Int rawData Json // Stores remaining tab/live/video metrics createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model TiktokProductSku { id String @id // SKU ID productId String skuName String status String gmv Float skuOrders Int itemsSold Int rawData Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model RawMaterial { id Int @id @default(autoincrement()) name String @unique unit String // e.g., 'kg', 'liter', 'piece' pricePerUnit Float bomItems BomItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model BomItem { id Int @id @default(autoincrement()) productVariantId Int productVariant ProductVariant @relation(fields: [productVariantId], references: [id], onDelete: Cascade) rawMaterialId Int rawMaterial RawMaterial @relation(fields: [rawMaterialId], references: [id], onDelete: Cascade) quantity Float createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([productVariantId, rawMaterialId]) } model ShopeeOrder { orderId String @id shop String @default("hom-aroma") status String buyerUsername String? orderCreatedAt String? paymentTime String? paymentMethod String? trackingNo String? totalAmount Float @default(0) buyerPaidAmount Float @default(0) shippingFee Float @default(0) commission Float @default(0) completedAt String? items ShopeeOrderItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ShopeeOrderItem { id Int @id @default(autoincrement()) orderId String order ShopeeOrder @relation(fields: [orderId], references: [orderId], onDelete: Cascade) parentSku String? productName String skuRef String? variantName String? originalPrice Float @default(0) dealPrice Float @default(0) quantity Int @default(1) totalPayment Float @default(0) rawData Json } // ── TikTok Orders ────────────────────────────────────────────────────────────── model TiktokOrder { id Int @id @default(autoincrement()) orderId String @unique shop String @default("hom-aroma") status String // Order Status (Thai) subStatus String? // Order Substatus orderType String? // Normal / Pre-order cancelBy String? cancelReason String? fulfillmentType String? warehouseName String? trackingId String? deliveryOption String? shippingProvider String? buyerUsername String? paymentMethod String? // Financials (order-level) orderAmount Float @default(0) // Total paid by buyer refundAmount Float @default(0) shippingFee Float @default(0) // Shipping Fee After Discount originalShipping Float @default(0) taxes Float @default(0) // Settlement Financials commissionFee Float @default(0) // ค่าคอมมิชชั่น TikTok affiliateFee Float @default(0) // ค่าคอมมิชชั่นแอฟฟิลิเอต netSettlement Float @default(0) // ยอดเงินโอนจริงสุทธิ settledAt String? // วันที่โอนเงิน isSettled Boolean @default(false) // Geography province String? district String? country String? // Timestamps createdTime String? paidTime String? rtsTime String? shippedTime String? deliveredTime String? cancelledTime String? items TiktokOrderItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([shop]) @@index([status]) @@index([createdTime]) } model TiktokOrderItem { id Int @id @default(autoincrement()) orderId String order TiktokOrder @relation(fields: [orderId], references: [orderId], onDelete: Cascade) skuId String? sellerSku String? productName String variation String? quantity Int @default(1) skuQtyReturn Int @default(0) unitOriginalPrice Float @default(0) subtotalBeforeDiscount Float @default(0) platformDiscount Float @default(0) sellerDiscount Float @default(0) subtotalAfterDiscount Float @default(0) // Revenue ระดับ SKU productCategory String? weight Float @default(0) } model TiktokSettlement { id Int @id @default(autoincrement()) orderId String @unique // หมายเลขคำสั่งซื้อ/การปรับ shop String @default("hom-aroma") transactionType String? // ประเภทธุรกรรม (คำสั่งซื้อ / การปรับยอด) orderCreatedTime String? // เวลาที่สร้างคำสั่งซื้อ orderSettledTime String? // เวลาที่ชำระคำสั่งซื้อ / วันที่โอนเงิน currency String @default("THB") // Financial breakdowns settlementAmount Float @default(0) // ยอดการชำระเงินทั้งหมด (Net Payout) grossRevenue Float @default(0) // รายได้ทั้งหมด subtotalAfterSellerDiscount Float @default(0) // ยอดรวมค่าสินค้าหลังหักส่วนลดจากผู้ขาย subtotalBeforeDiscount Float @default(0) // ยอดรวมค่าสินค้าก่อนหักส่วนลด sellerDiscount Float @default(0) // ส่วนลดจากร้านค้า refundAmount Float @default(0) // ยอดรวมเงินคืน // Fees totalFee Float @default(0) // ค่าธรรมเนียมทั้งหมด orderFee Float @default(0) // ค่าธรรมเนียมคำสั่งซื้อ commissionFee Float @default(0) // ค่าคอมมิชชั่น TikTok Shop affiliateCommission Float @default(0) // ค่าคอมมิชชั่นแอฟฟิลิเอต affiliatePartnerFee Float @default(0) // ค่าคอมมิชชั่นของพาร์ทเนอร์แอฟฟิลิเอต growthSupportFee Float @default(0) // ค่าธรรมเนียมสนับสนุนการเติบโตของร้านค้า shippingFeeActual Float @default(0) // ค่าธรรมเนียมการจัดส่งจริง platformShippingDiscount Float @default(0) // ส่วนลดค่าธรรมเนียมการจัดส่งจากแพลตฟอร์ม customerPayment Float @default(0) // การชำระเงินของลูกค้า rawData Json? // Full raw row data for audit createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([shop]) @@index([orderSettledTime]) @@index([orderId]) } // ── Lazada Orders ────────────────────────────────────────────────────────────── model LazadaOrder { id Int @id @default(autoincrement()) orderNumber String @unique shop String @default("hom-aroma") createTime String? updateTime String? deliveredDate String? customerName String? shippingCity String? shippingPostCode String? shippingRegion String? payMethod String? status String? // Based on items or first item trackingCode String? shippingProvider String? // Aggregate from items orderAmount Float @default(0) // sum(paidPrice) shippingFee Float @default(0) // sum(shippingFee) items LazadaOrderItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([shop]) @@index([status]) @@index([createTime]) } model LazadaOrderItem { id Int @id @default(autoincrement()) orderNumber String order LazadaOrder @relation(fields: [orderNumber], references: [orderNumber], onDelete: Cascade) orderItemId String @unique sellerSku String? lazadaSku String? itemName String variation String? unitPrice Float @default(0) paidPrice Float @default(0) sellerDiscountTotal Float @default(0) platformDiscountTotal Float @default(0) shippingFee Float @default(0) status String? } // ── Reviews ──────────────────────────────────────────────────────────────── model Review { id Int @id @default(autoincrement()) rating Int @default(5) comment String? productId Int product Product @relation(fields: [productId], references: [id], onDelete: Cascade) customerId Int? customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull) customerName String? isPublished Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([productId]) @@index([customerId]) } model Page { id Int @id @default(autoincrement()) slug String @unique // e.g., "home", "about-us" title String sections PageSection[] translations PageTranslation[] isPublished Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model PageTranslation { id Int @id @default(autoincrement()) pageId Int page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) langCode String title String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([pageId, langCode]) } model PageSection { id Int @id @default(autoincrement()) pageId Int page Page @relation(fields: [pageId], references: [id], onDelete: Cascade) type String // e.g., "HERO", "CATEGORIES", "PRODUCTS", "BANNER", "TEXT" order Int // Display order payload Json // Dynamic JSON payload isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model ProductDetailSection { id Int @id @default(autoincrement()) productId Int product Product @relation(fields: [productId], references: [id], onDelete: Cascade) type String order Int payload Json isActive Boolean @default(true) isLocked Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([productId, order]) @@index([productId, isActive]) } model NewsletterSubscription { id Int @id @default(autoincrement()) email String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Menu { id Int @id @default(autoincrement()) name String @unique // e.g., "header", "footer" payload Json? rootImage String? isActive Boolean @default(true) items MenuItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model MenuItem { id Int @id @default(autoincrement()) menuId Int menu Menu @relation(fields: [menuId], references: [id], onDelete: Cascade) label String url String order Int @default(0) columnImage String? hoverImage String? translations MenuItemTranslation[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model MenuItemTranslation { id Int @id @default(autoincrement()) menuItemId Int menuItem MenuItem @relation(fields: [menuItemId], references: [id], onDelete: Cascade) langCode String label String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([menuItemId, langCode]) } model Media { id Int @id @default(autoincrement()) filename String path String mimeType String sizeBytes Int createdAt DateTime @default(now()) } // ── Contact Messages ──────────────────────────────────────────────────────── model ContactMessage { id Int @id @default(autoincrement()) name String email String message String isRead Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // ── Marketplace Products & SKU Mapping ────────────────────────────────────────── model MarketplaceProduct { id Int @id @default(autoincrement()) channel String // "tiktok" | "shopee" | "lazada" shop String @default("hom-aroma") marketplaceSku String @default("") // sellerSku / skuRef / skuId productName String // Item name on marketplace variation String @default("") // Variation / flavor / size avgPrice Float @default(0) totalSoldQty Int @default(0) totalRevenue Float @default(0) firstSoldAt String? lastSoldAt String? // Link to Master Core Catalog masterVariantId Int? masterVariant ProductVariant? @relation(fields: [masterVariantId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([channel, shop, productName, variation, marketplaceSku]) @@index([channel]) @@index([shop]) @@index([marketplaceSku]) @@index([masterVariantId]) @@index([totalRevenue]) } // ── Marketplace Customers & CRM ─────────────────────────────────────────────── model MarketplaceCustomer { id Int @id @default(autoincrement()) channel String // "tiktok" | "shopee" | "lazada" shop String @default("hom-aroma") identifier String // Unique identifier per channel (username or name+postcode) displayName String // Display name on UI location String? // District, Province province String? district String? // Aggregated buyer stats orderCount Int @default(0) totalSpent Float @default(0) avgIntervalDays Float @default(0) firstOrderDate String? lastOrderDate String? predictedNextOrderDate String? topProducts Json? // Stored as JSON array: string[] // CRM Relationship crm MarketplaceCustomerCrm? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([channel, identifier]) @@index([channel]) @@index([totalSpent]) @@index([orderCount]) } model MarketplaceCustomerCrm { id Int @id @default(autoincrement()) customerId Int @unique customer MarketplaceCustomer @relation(fields: [customerId], references: [id], onDelete: Cascade) // Tracking status status String @default("pending") // "pending" | "contacted" | "converted" | "unresponsive" | "do_not_disturb" strategyType String @default("due_restock") // "due_restock" | "win_back" | "vip_reward" | "nurture" // Campaign & contact details promoCodeSent String? // e.g., "REPEAT10" discountOffered String? // e.g., "ส่วนลด 10%" lastContactedAt DateTime? nextFollowUpDate DateTime? notes String? assignedTo String? activities CustomerCrmActivity[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status]) @@index([strategyType]) @@index([nextFollowUpDate]) } model CustomerCrmActivity { id Int @id @default(autoincrement()) crmId Int crm MarketplaceCustomerCrm @relation(fields: [crmId], references: [id], onDelete: Cascade) action String // "STATUS_CHANGE" | "SENT_PROMO" | "CALL_NOTE" | "CHAT_NOTE" content String author String? createdAt DateTime @default(now()) @@index([crmId]) }