Rust discount function (one automatic discount evaluating all offers in priority order, parity with the Odoo engine), React Router admin with Polaris, theme app extension and app proxy pages, webhooks incl. GDPR, managed-pricing limits, and a Docker Compose stack (Postgres + Caddy HTTPS) for a single VPS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
5.7 KiB
Plaintext
181 lines
5.7 KiB
Plaintext
// Bundle Offers app database (brief §14). Postgres.
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model Session {
|
|
id String @id
|
|
shop String
|
|
state String
|
|
isOnline Boolean @default(false)
|
|
scope String?
|
|
expires DateTime?
|
|
accessToken String
|
|
userId BigInt?
|
|
firstName String?
|
|
lastName String?
|
|
email String?
|
|
accountOwner Boolean @default(false)
|
|
locale String?
|
|
collaborator Boolean? @default(false)
|
|
emailVerified Boolean? @default(false)
|
|
refreshToken String?
|
|
refreshTokenExpires DateTime?
|
|
}
|
|
|
|
model Shop {
|
|
id String @id @default(cuid())
|
|
domain String @unique
|
|
planCode String @default("free") // free | starter | growth | plus
|
|
currency String @default("USD")
|
|
timezone String @default("UTC") // IANA
|
|
moneyFormat String @default("${{amount}}")
|
|
discountNodeId String?
|
|
appInstallationId String?
|
|
/// Settings JSON: exclusion tag, combinesWith, bxgy allocation, cart behaviour, design tokens
|
|
settings Json @default("{}")
|
|
/// Next offer start/end boundary: the scheduler republishes at this time
|
|
nextSyncAt DateTime?
|
|
lastPublishedAt DateTime?
|
|
configBytes Int @default(0)
|
|
storefrontBytes Int @default(0)
|
|
publishError String?
|
|
/// Free plan: bundle orders in the current month
|
|
monthKey String?
|
|
monthOrders Int @default(0)
|
|
reviewPromptShown Boolean @default(false)
|
|
installedAt DateTime @default(now())
|
|
uninstalledAt DateTime?
|
|
offers Offer[]
|
|
usages OrderUsage[]
|
|
audit AuditLog[]
|
|
}
|
|
|
|
model Offer {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
/// short id used in the function config and storefront data
|
|
code String
|
|
handle String
|
|
oldHandles String[] @default([])
|
|
name String
|
|
badge String?
|
|
description String?
|
|
/// { "fr": { "name": "...", "badge": "...", "description": "..." } }
|
|
i18n Json @default("{}")
|
|
sequence Int @default(10)
|
|
active Boolean @default(true)
|
|
published Boolean @default(false)
|
|
/// ISO country codes (market targeting); empty = everywhere
|
|
countries String[] @default([])
|
|
type String @default("fixed") // fixed | mix | bxgy
|
|
discountType String @default("percent") // percent | amount | fixed_price
|
|
discountValue Decimal @default(0) @db.Decimal(12, 2)
|
|
mixQty Int @default(3)
|
|
buyQty Int @default(2)
|
|
getQty Int @default(1)
|
|
getPercent Decimal @default(100) @db.Decimal(5, 2)
|
|
startsAt DateTime?
|
|
endsAt DateTime?
|
|
maxPerOrder Int @default(0)
|
|
showHome Boolean @default(true)
|
|
showProduct Boolean @default(true)
|
|
showCartHints Boolean @default(true)
|
|
imageUrl String?
|
|
/// Unpublished automatically because the plan limit was exceeded
|
|
limitedByPlan Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
lines OfferLine[]
|
|
scopes OfferScope[]
|
|
usages OrderUsage[]
|
|
|
|
@@unique([shopId, code])
|
|
@@unique([shopId, handle])
|
|
@@index([shopId, sequence])
|
|
}
|
|
|
|
model OfferLine {
|
|
id String @id @default(cuid())
|
|
offerId String
|
|
offer Offer @relation(fields: [offerId], references: [id], onDelete: Cascade)
|
|
sequence Int @default(10)
|
|
productId String
|
|
/// null = any variant of the product
|
|
variantId String?
|
|
quantity Int @default(1)
|
|
/// display cache (from the resource picker / products webhooks)
|
|
title String?
|
|
handle String?
|
|
imageUrl String?
|
|
}
|
|
|
|
model OfferScope {
|
|
id String @id @default(cuid())
|
|
offerId String
|
|
offer Offer @relation(fields: [offerId], references: [id], onDelete: Cascade)
|
|
role String // mix | buy | get
|
|
kind String // product | variant | collection
|
|
refId String // numeric id
|
|
/// product id for variants
|
|
parentId String?
|
|
title String?
|
|
handle String?
|
|
imageUrl String?
|
|
|
|
@@index([offerId, role])
|
|
}
|
|
|
|
model OrderUsage {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
orderId String
|
|
orderName String?
|
|
offerId String?
|
|
offer Offer? @relation(fields: [offerId], references: [id], onDelete: SetNull)
|
|
offerName String
|
|
discount Decimal @db.Decimal(12, 2)
|
|
bundleRevenue Decimal @db.Decimal(12, 2)
|
|
orderTotal Decimal @db.Decimal(12, 2)
|
|
currency String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([shopId, orderId, offerName])
|
|
@@index([shopId, createdAt])
|
|
}
|
|
|
|
/// Orders without a bundle, for the "AOV with vs without bundles" metric
|
|
model OrderStat {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
orderId String
|
|
total Decimal @db.Decimal(12, 2)
|
|
hasBundle Boolean
|
|
currency String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([shopId, orderId])
|
|
@@index([shopId, createdAt])
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
offerId String?
|
|
staffUser String?
|
|
field String
|
|
oldValue String?
|
|
newValue String?
|
|
at DateTime @default(now())
|
|
|
|
@@index([shopId, offerId, at])
|
|
}
|