// 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[] accessLogs DataAccessLog[] } 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> 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? /// Encrypted at rest (app/lib/pii.server.ts). Query email/phone through the *Hash columns. customerName String? customerEmail String? customerPhone String? address Json? customerEmailHash String? customerPhoneHash String? 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]) @@index([shopId, customerEmailHash]) } 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: | 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 /// Encrypted at rest (app/lib/pii.server.ts). Query through emailHash / phoneHash. email String? phone String? name String? emailHash String? phoneHash 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]) } /// Who looked at customers' personal data, and when (staff views/exports, customer self-service). /// Kept 12 months (worker), deleted with the shop on shop/redact. model DataAccessLog { id String @id @default(cuid()) shopId String shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade) actor String // merchant: | customer: | system action String // bookings.list | booking.view | bookings.export | runsheet.view | customer.booking.view target String? at DateTime @default(now()) @@index([shopId, at]) } /// 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()) }