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