245 lines
7.3 KiB
Plaintext
245 lines
7.3 KiB
Plaintext
// Datewise data model. Postgres is required: reservations rely on row locks.
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
previewFeatures = ["queryCompiler", "driverAdapters"]
|
|
engineType = "client"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// Required by @shopify/shopify-app-session-storage-prisma
|
|
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
|
|
currency String @default("CAD")
|
|
timezone String @default("America/Toronto")
|
|
/// ShopSettings JSON (holds, reschedule policy, reminders, mixed-cart policy, widget text)
|
|
settings Json
|
|
/// Incremented on every config change; stored on bookings as the rule version used.
|
|
configVersion Int @default(1)
|
|
plan String @default("free")
|
|
feeProductId String?
|
|
installedAt DateTime @default(now())
|
|
uninstalledAt DateTime?
|
|
locations Location[]
|
|
rules ProductRule[]
|
|
peakDays PeakDay[]
|
|
bookings Booking[]
|
|
holds Hold[]
|
|
counters CapacityCounter[]
|
|
waitlist WaitlistEntry[]
|
|
reminderJobs ReminderJob[]
|
|
events BookingEvent[]
|
|
}
|
|
|
|
model Location {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
shopifyLocationId String?
|
|
name String
|
|
address String
|
|
timezone String
|
|
lat Float?
|
|
lng Float?
|
|
enabled Boolean @default(true)
|
|
priority Int @default(1)
|
|
pickupInstructions String?
|
|
/// Partial<Record<Method, MethodConfig>>
|
|
methods Json
|
|
/// DateException[]
|
|
exceptions Json @default("[]")
|
|
/// DeliveryZone[]
|
|
zones Json @default("[]")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([shopId])
|
|
}
|
|
|
|
model ProductRule {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
name String
|
|
enabled Boolean @default(true)
|
|
priority Int @default(10)
|
|
target Json
|
|
action Json
|
|
message String?
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([shopId])
|
|
}
|
|
|
|
model PeakDay {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
name String
|
|
dates String[]
|
|
locationIds String[]
|
|
methods String[]
|
|
dailyCapacity Int?
|
|
slotCapacity Int?
|
|
extraPrepMinutes Int @default(0)
|
|
cutoffDaysBefore Int?
|
|
cutoffTime String?
|
|
surcharge Int @default(0)
|
|
waitlist Boolean @default(false)
|
|
message String?
|
|
|
|
@@index([shopId])
|
|
}
|
|
|
|
/// One row per (location, method, date, slot). slot = "*" is the day total.
|
|
/// Counts confirmed bookings + live holds. Mutated only inside row-locked transactions.
|
|
model CapacityCounter {
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
locationId String
|
|
method String
|
|
date String
|
|
slot String
|
|
count Int @default(0)
|
|
|
|
@@id([shopId, locationId, method, date, slot])
|
|
}
|
|
|
|
model Hold {
|
|
token String @id
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
locationId String
|
|
method String
|
|
date String
|
|
slot String
|
|
expiresAt DateTime
|
|
/// Fee quoted to the customer for this selection (minor units); re-checked against the paid order.
|
|
fee Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([shopId, locationId, method, date])
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
model Booking {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
orderId String?
|
|
orderName String?
|
|
status String // confirmed | needs_review | cancelled | fulfilled
|
|
reviewReason String?
|
|
method String
|
|
locationId String?
|
|
date String? // local YYYY-MM-DD
|
|
slotStart String? // local HH:MM
|
|
slotEnd String?
|
|
timezone String
|
|
startUtc DateTime?
|
|
endUtc DateTime?
|
|
customerName String?
|
|
customerEmail String?
|
|
customerPhone String?
|
|
address Json?
|
|
items Json?
|
|
fee Int @default(0)
|
|
peakDayId String?
|
|
rescheduleCount Int @default(0)
|
|
configVersion Int
|
|
/// Snapshot of what the customer agreed to (method, location, slot, fee, rules matched)
|
|
snapshot Json
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
events BookingEvent[]
|
|
reminderJobs ReminderJob[]
|
|
|
|
@@unique([shopId, orderId])
|
|
@@index([shopId, date])
|
|
@@index([shopId, status])
|
|
}
|
|
|
|
model BookingEvent {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
bookingId String
|
|
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
|
actor String // system | merchant:<staff id> | customer
|
|
action String // confirmed | flagged | rescheduled | cancelled | resolved
|
|
before Json?
|
|
after Json?
|
|
at DateTime @default(now())
|
|
|
|
@@index([bookingId])
|
|
}
|
|
|
|
model WaitlistEntry {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
locationId String
|
|
method String
|
|
date String
|
|
slot String // "*" = any slot that day
|
|
email String?
|
|
phone String?
|
|
name String?
|
|
createdAt DateTime @default(now())
|
|
offeredAt DateTime?
|
|
/// Hold token reserved for this person when a place is offered.
|
|
offerToken String?
|
|
|
|
@@index([shopId, locationId, method, date])
|
|
}
|
|
|
|
model ReminderJob {
|
|
id String @id @default(cuid())
|
|
shopId String
|
|
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
|
|
bookingId String
|
|
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
|
policyId String
|
|
channel String
|
|
sendAt DateTime
|
|
body String
|
|
status String @default("pending") // pending | sent | failed | cancelled
|
|
attempts Int @default(0)
|
|
lastError String?
|
|
sentAt DateTime?
|
|
|
|
@@index([status, sendAt])
|
|
}
|
|
|
|
/// Webhook idempotency: Shopify may deliver the same event more than once.
|
|
model ProcessedWebhook {
|
|
id String @id // X-Shopify-Event-Id (falls back to webhook id)
|
|
topic String
|
|
shop String
|
|
receivedAt DateTime @default(now())
|
|
}
|