930 lines
33 KiB
Plaintext
930 lines
33 KiB
Plaintext
// 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?
|
|
layoutType String @default("split_showcase")
|
|
media CategoryMedia[]
|
|
products Product[]
|
|
attributes AttributeDefinition[]
|
|
translations CategoryTranslation[]
|
|
deletedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([deletedAt])
|
|
}
|
|
|
|
model CategoryMedia {
|
|
id Int @id @default(autoincrement())
|
|
categoryId Int
|
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
|
role String
|
|
url String
|
|
width Int?
|
|
height Int?
|
|
focalX Float @default(0.5)
|
|
focalY Float @default(0.5)
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([categoryId, role])
|
|
@@index([categoryId, sortOrder])
|
|
}
|
|
|
|
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[]
|
|
isDeleted Boolean @default(false)
|
|
deletedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([isDeleted])
|
|
@@index([categoryId, isDeleted])
|
|
}
|
|
|
|
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
|
|
mobileUrl 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?
|
|
footerLayout Json?
|
|
scrollRevealEnabled Boolean @default(true)
|
|
scrollRevealStyle String @default("fade-up")
|
|
scrollRevealDuration Int @default(700)
|
|
headerStyle String @default("single_line")
|
|
defaultSeoTitle String?
|
|
defaultSeoDescription String?
|
|
defaultOgImage String?
|
|
brandPrompt String?
|
|
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?
|
|
defaultSeoTitle String?
|
|
defaultSeoDescription String?
|
|
brandPrompt 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 BrandKnowledge {
|
|
id Int @id @default(1)
|
|
brandName String @default("AKÉRIA")
|
|
tagline String?
|
|
story String?
|
|
targetAudience String?
|
|
toneOfVoice String?
|
|
visualArtPrompt String?
|
|
productUsps String?
|
|
marketingGuidelines String?
|
|
customRules String?
|
|
masterPromptOverride String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Order {
|
|
id Int @id @default(autoincrement())
|
|
channel String @default("ONLINE") // "ONLINE" | "POS"
|
|
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(0)
|
|
totalAmount Float
|
|
status String @default("PENDING_PAYMENT")
|
|
paymentMethod String? @default("CASH")
|
|
receivedAmount Float?
|
|
changeAmount Float?
|
|
cashierId Int?
|
|
paymentSlipUrl String?
|
|
trackingNo String?
|
|
|
|
customerId Int?
|
|
customer Customer? @relation(fields: [customerId], references: [id])
|
|
|
|
items OrderItem[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([channel])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
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
|
|
seoTitle String?
|
|
seoDescription String?
|
|
seoKeywords String?
|
|
ogImageUrl 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
|
|
seoTitle String?
|
|
seoDescription String?
|
|
seoKeywords 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])
|
|
}
|
|
|