bundle-offers-app/prisma/schema.prisma
metatroncubeswdev 38c03cd1ae Encrypt order data at rest; add terms of service with data processing terms
Protected customer data (Level 1): order ids are stored as keyed HMACs, order numbers
and ids for admin links as AES-256-GCM ciphertext (DATA_ENCRYPTION_KEY). New public
/terms page with processor terms; privacy policy links to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:29:48 -04:00

186 lines
6.0 KiB
Plaintext

// Offerlyst 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)
/// keyed hash of the Shopify order id (app/lib/crypto.server.ts)
orderId String
/// AES-GCM encrypted order id, for the admin link
orderRef String?
/// AES-GCM encrypted order number (#1001)
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
/// keyed hash of the Shopify order id
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])
}