Stage M5 and Stage M6
This commit is contained in:
@@ -208,6 +208,7 @@ model Customer {
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
addresses Address[]
|
||||
orders Order[]
|
||||
|
||||
@@map("customers")
|
||||
}
|
||||
@@ -504,6 +505,7 @@ model Product {
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3)
|
||||
|
||||
searchDocuments SearchDocument[]
|
||||
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
|
||||
primaryCategory Category? @relation(fields: [primaryCategoryId], references: [id], onDelete: SetNull)
|
||||
options ProductOption[]
|
||||
@@ -601,6 +603,7 @@ model ProductVariant {
|
||||
optionValues ProductVariantOptionValue[]
|
||||
stockLevels StockLevel[]
|
||||
stockMovements StockMovement[]
|
||||
orderLines OrderLine[]
|
||||
|
||||
@@index([productId, position])
|
||||
@@index([status])
|
||||
@@ -864,3 +867,167 @@ model ProductAttributeTranslation {
|
||||
@@id([attributeId, locale])
|
||||
@@map("product_attribute_translations")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Commerce — orders (M5)
|
||||
//
|
||||
// Carts are deliberately absent: a guest cart lives in Redis and holds only
|
||||
// variant ids and quantities (ADR-0010). Prices are recomputed from the catalog
|
||||
// on every read, so a cart can never carry a stale or tampered price into an
|
||||
// order.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum OrderStatus {
|
||||
/// Placed, awaiting payment. Stock is reserved from this moment.
|
||||
PENDING
|
||||
CONFIRMED
|
||||
FULFILLED
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
UNPAID
|
||||
PAID
|
||||
PARTIALLY_REFUNDED
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
enum FulfillmentStatus {
|
||||
UNFULFILLED
|
||||
PARTIALLY_FULFILLED
|
||||
FULFILLED
|
||||
}
|
||||
|
||||
/// A placed order.
|
||||
///
|
||||
/// Every customer-facing and catalog-facing value is SNAPSHOT here rather than
|
||||
/// joined at read time. An order is a record of what was agreed, and it has to
|
||||
/// stay readable after the product is renamed, repriced, archived or the
|
||||
/// customer edits their address book. The `variantId` FK exists for reporting
|
||||
/// and returns, never for rendering the order.
|
||||
model Order {
|
||||
id String @id @default(uuid(7)) @db.Uuid
|
||||
|
||||
/// Human-facing reference, formatted for display as "SP-000123". Kept as an
|
||||
/// integer so it is monotonic and cheap to look up; the prefix is
|
||||
/// presentation and lives in the mapper.
|
||||
number Int @unique @default(autoincrement())
|
||||
|
||||
/// Null for a guest checkout. Guests are identified by email + order number.
|
||||
customerId String? @map("customer_id") @db.Uuid
|
||||
|
||||
email String @db.VarChar(255)
|
||||
phone String @db.VarChar(20)
|
||||
|
||||
status OrderStatus @default(PENDING)
|
||||
paymentStatus PaymentStatus @default(UNPAID) @map("payment_status")
|
||||
fulfillmentStatus FulfillmentStatus @default(UNFULFILLED) @map("fulfillment_status")
|
||||
|
||||
/// ---- Money, integer minor units throughout (ADR-0011) ------------------
|
||||
currency Currency @default(VND)
|
||||
subtotalAmount Int @map("subtotal_amount")
|
||||
/// Zero until promotions land (M7); the column exists so the total is always
|
||||
/// the sum of named parts rather than an unexplained number.
|
||||
discountAmount Int @default(0) @map("discount_amount")
|
||||
shippingAmount Int @default(0) @map("shipping_amount")
|
||||
taxAmount Int @default(0) @map("tax_amount")
|
||||
totalAmount Int @map("total_amount")
|
||||
|
||||
/// ---- Shipping address, snapshot ----------------------------------------
|
||||
shipFullName String @map("ship_full_name") @db.VarChar(160)
|
||||
shipPhone String @map("ship_phone") @db.VarChar(20)
|
||||
shipLine1 String @map("ship_line1") @db.VarChar(255)
|
||||
shipLine2 String? @map("ship_line2") @db.VarChar(255)
|
||||
shipWard String? @map("ship_ward") @db.VarChar(120)
|
||||
shipDistrict String? @map("ship_district") @db.VarChar(120)
|
||||
shipProvince String @map("ship_province") @db.VarChar(120)
|
||||
shipCountryCode String @default("VN") @map("ship_country_code") @db.Char(2)
|
||||
shipPostalCode String? @map("ship_postal_code") @db.VarChar(20)
|
||||
|
||||
customerNote String? @map("customer_note") @db.VarChar(1000)
|
||||
|
||||
placedAt DateTime @default(now()) @map("placed_at") @db.Timestamptz(3)
|
||||
confirmedAt DateTime? @map("confirmed_at") @db.Timestamptz(3)
|
||||
cancelledAt DateTime? @map("cancelled_at") @db.Timestamptz(3)
|
||||
cancelReason String? @map("cancel_reason") @db.VarChar(500)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
lines OrderLine[]
|
||||
|
||||
@@index([customerId])
|
||||
@@index([email])
|
||||
@@index([status, placedAt])
|
||||
@@map("orders")
|
||||
}
|
||||
|
||||
/// One purchased variant, frozen at the moment of purchase.
|
||||
model OrderLine {
|
||||
id String @id @default(uuid(7)) @db.Uuid
|
||||
orderId String @map("order_id") @db.Uuid
|
||||
|
||||
/// Nulled rather than cascading if a variant is ever hard-deleted — losing
|
||||
/// the reporting link is survivable, losing the order line is not.
|
||||
variantId String? @map("variant_id") @db.Uuid
|
||||
|
||||
/// ---- Snapshot ----------------------------------------------------------
|
||||
productName String @map("product_name") @db.VarChar(255)
|
||||
variantTitle String @map("variant_title") @db.VarChar(255)
|
||||
sku String @db.VarChar(64)
|
||||
imageUrl String? @map("image_url") @db.VarChar(500)
|
||||
|
||||
unitAmount Int @map("unit_amount")
|
||||
quantity Int
|
||||
lineAmount Int @map("line_amount")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
variant ProductVariant? @relation(fields: [variantId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([orderId])
|
||||
@@index([variantId])
|
||||
@@map("order_lines")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search (M6)
|
||||
//
|
||||
// Owned exclusively by SearchModule. It is a *projection*: every row is
|
||||
// derivable from the catalog and can be rebuilt from scratch at any time, which
|
||||
// is what lets the module be extracted later without taking catalog tables with
|
||||
// it (ADR-0012).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One searchable document per product per locale.
|
||||
///
|
||||
/// The text is split by weight rather than concatenated, because a match on a
|
||||
/// product's name should outrank a match buried in its description. The
|
||||
/// `document` column is GENERATED from these three by the database — see the
|
||||
/// migration — so an index can never drift from the text it indexes.
|
||||
model SearchDocument {
|
||||
productId String @map("product_id") @db.Uuid
|
||||
locale Locale
|
||||
|
||||
/// Weight A — the product name.
|
||||
title String @db.VarChar(255)
|
||||
/// Weight B — brand, category, colourways, sizes, SKUs. Short, high-signal.
|
||||
keywords String
|
||||
/// Weight C — descriptions. Long, low-signal, still worth matching.
|
||||
body String
|
||||
|
||||
/// Maintained by Postgres from the columns above. Declared here only so
|
||||
/// Prisma knows it exists and leaves it alone; it is read and written with
|
||||
/// raw SQL in the repository.
|
||||
document Unsupported("tsvector")?
|
||||
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([productId, locale])
|
||||
@@map("search_documents")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user