added availability
This commit is contained in:
318
prisma/schema.prisma
Normal file
318
prisma/schema.prisma
Normal file
@@ -0,0 +1,318 @@
|
||||
// TouchBase schema. See /Users/noise/Documents/obsidian/Massage/Initial.md §4 for design rationale.
|
||||
// All timestamps are timestamptz (UTC at rest); WorkingHours is the only "wall-clock-local" model.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Identity & roles
|
||||
// ============================================================
|
||||
|
||||
enum Role {
|
||||
CUSTOMER
|
||||
THERAPIST
|
||||
ADMIN
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
emailVerified DateTime? @db.Timestamptz(3)
|
||||
name String
|
||||
phone String?
|
||||
role Role @default(CUSTOMER)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
deletedAt DateTime? @db.Timestamptz(3)
|
||||
|
||||
customer Customer?
|
||||
therapist Therapist?
|
||||
bookings Booking[] @relation("CustomerBookings")
|
||||
audits AuditLog[] @relation("ActorAudit")
|
||||
|
||||
@@index([role])
|
||||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model Customer {
|
||||
userId String @id
|
||||
notes String? // Front-desk notes. Sensitive — column-level encrypt before prod.
|
||||
stripeCustomerId String? @unique
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Therapist {
|
||||
userId String @id
|
||||
bio String?
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
|
||||
tags TherapistTag[]
|
||||
workingHours WorkingHours[]
|
||||
overrides AvailabilityOverride[]
|
||||
services ServiceTherapist[]
|
||||
bookings Booking[]
|
||||
}
|
||||
|
||||
model TherapistTag {
|
||||
therapistId String
|
||||
tag String
|
||||
|
||||
therapist Therapist @relation(fields: [therapistId], references: [userId], onDelete: Cascade)
|
||||
|
||||
@@id([therapistId, tag])
|
||||
@@index([tag])
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Resources
|
||||
// ============================================================
|
||||
|
||||
model Room {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
tags RoomTag[]
|
||||
blocks RoomBlock[]
|
||||
bookings Booking[]
|
||||
}
|
||||
|
||||
model RoomTag {
|
||||
roomId String
|
||||
tag String
|
||||
|
||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([roomId, tag])
|
||||
@@index([tag])
|
||||
}
|
||||
|
||||
model RoomBlock {
|
||||
id String @id @default(cuid())
|
||||
roomId String
|
||||
startsAt DateTime @db.Timestamptz(3)
|
||||
endsAt DateTime @db.Timestamptz(3)
|
||||
reason String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([roomId, startsAt])
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Services
|
||||
// ============================================================
|
||||
|
||||
model Service {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
durationMin Int
|
||||
bufferAfterMin Int @default(15)
|
||||
priceCents Int
|
||||
depositCents Int @default(0)
|
||||
active Boolean @default(true)
|
||||
requiredTherapistTags String[] @default([])
|
||||
requiredRoomTags String[] @default([])
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
therapists ServiceTherapist[]
|
||||
bookings Booking[]
|
||||
}
|
||||
|
||||
// Explicit allowlist of therapists who perform a service.
|
||||
// Tag intersection is the necessary condition; this is the additional opt-in.
|
||||
model ServiceTherapist {
|
||||
serviceId String
|
||||
therapistId String
|
||||
|
||||
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
|
||||
therapist Therapist @relation(fields: [therapistId], references: [userId], onDelete: Cascade)
|
||||
|
||||
@@id([serviceId, therapistId])
|
||||
@@index([therapistId])
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Availability
|
||||
// ============================================================
|
||||
|
||||
model WorkingHours {
|
||||
id String @id @default(cuid())
|
||||
therapistId String
|
||||
weekday Int // 0=Sun .. 6=Sat
|
||||
startMin Int // minutes from midnight, in APP_TZ (practice-local wall clock)
|
||||
endMin Int
|
||||
effectiveFrom DateTime? @db.Timestamptz(3)
|
||||
effectiveTo DateTime? @db.Timestamptz(3)
|
||||
|
||||
therapist Therapist @relation(fields: [therapistId], references: [userId], onDelete: Cascade)
|
||||
|
||||
@@index([therapistId, weekday])
|
||||
}
|
||||
|
||||
enum OverrideKind {
|
||||
BLOCK // PTO, sick — blocks an underlying working hours interval
|
||||
EXTRA_HOURS // ad-hoc availability outside normal working hours
|
||||
}
|
||||
|
||||
model AvailabilityOverride {
|
||||
id String @id @default(cuid())
|
||||
therapistId String
|
||||
startsAt DateTime @db.Timestamptz(3)
|
||||
endsAt DateTime @db.Timestamptz(3)
|
||||
kind OverrideKind
|
||||
reason String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
therapist Therapist @relation(fields: [therapistId], references: [userId], onDelete: Cascade)
|
||||
|
||||
@@index([therapistId, startsAt])
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Bookings
|
||||
// ============================================================
|
||||
|
||||
enum BookingStatus {
|
||||
HOLD
|
||||
CONFIRMED
|
||||
COMPLETED
|
||||
NO_SHOW
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
NONE
|
||||
PENDING
|
||||
AUTHORIZED
|
||||
CAPTURED
|
||||
REFUNDED
|
||||
FAILED
|
||||
}
|
||||
|
||||
model Booking {
|
||||
id String @id @default(cuid())
|
||||
customerId String
|
||||
therapistId String
|
||||
roomId String
|
||||
serviceId String
|
||||
|
||||
startsAt DateTime @db.Timestamptz(3)
|
||||
endsAt DateTime @db.Timestamptz(3)
|
||||
// = endsAt + service.bufferAfterMin. Stored on the row so the room exclusion
|
||||
// constraint is single-column without a join. Recompute when service buffer changes.
|
||||
roomReleasedAt DateTime @db.Timestamptz(3)
|
||||
|
||||
status BookingStatus @default(HOLD)
|
||||
holdExpiresAt DateTime? @db.Timestamptz(3)
|
||||
|
||||
priceCents Int @default(0)
|
||||
depositCents Int @default(0)
|
||||
paymentStatus PaymentStatus @default(NONE)
|
||||
stripePaymentIntentId String?
|
||||
|
||||
notes String? // Front-desk notes specific to this booking
|
||||
cancelledAt DateTime? @db.Timestamptz(3)
|
||||
cancelledBy String? // user id (free text — actor may be a system process)
|
||||
cancelReason String?
|
||||
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
customer User @relation("CustomerBookings", fields: [customerId], references: [id], onDelete: Restrict)
|
||||
therapist Therapist @relation(fields: [therapistId], references: [userId], onDelete: Restrict)
|
||||
room Room @relation(fields: [roomId], references: [id], onDelete: Restrict)
|
||||
service Service @relation(fields: [serviceId], references: [id], onDelete: Restrict)
|
||||
|
||||
payments Payment[]
|
||||
|
||||
@@index([startsAt])
|
||||
@@index([therapistId, startsAt])
|
||||
@@index([roomId, startsAt])
|
||||
@@index([status, holdExpiresAt])
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Payments
|
||||
// ============================================================
|
||||
|
||||
enum PaymentKind {
|
||||
DEPOSIT
|
||||
BALANCE
|
||||
REFUND
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
bookingId String
|
||||
kind PaymentKind
|
||||
amountCents Int
|
||||
currency String @default("usd")
|
||||
stripePaymentIntentId String? @unique
|
||||
status PaymentStatus
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([bookingId])
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Notifications & audit
|
||||
// ============================================================
|
||||
|
||||
// Snapshot semantics: we record the email address as it was at send time
|
||||
// (user may change theirs later). No FK to User intentionally.
|
||||
model Notification {
|
||||
id String @id @default(cuid())
|
||||
userId String?
|
||||
bookingId String?
|
||||
channel String // "email"
|
||||
template String // "booking_confirmation", "reminder_24h", etc.
|
||||
to String // address snapshot
|
||||
subject String
|
||||
bodyHash String // hash of rendered body for audit; full body not retained long-term
|
||||
status String // "queued" | "sent" | "failed" | "bounced"
|
||||
providerId String?
|
||||
sentAt DateTime? @db.Timestamptz(3)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
@@index([bookingId])
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
actorId String?
|
||||
action String // "booking.created", "user.viewed_customer_notes", ...
|
||||
entityType String
|
||||
entityId String
|
||||
meta Json?
|
||||
ip String?
|
||||
ua String?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
actor User? @relation("ActorAudit", fields: [actorId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([entityType, entityId])
|
||||
@@index([actorId, createdAt])
|
||||
}
|
||||
Reference in New Issue
Block a user