bundle-offers-app/prisma/schema.prisma
metatroncubeswdev 365e45406a Bundle Offers Shopify app: discount function, admin, storefront, deploy stack
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>
2026-10-13 21:35:58 -04:00

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])
}