/opt/mawid/packages/db/prisma
Edit: /opt/mawid/packages/db/prisma/schema.prisma (8053B)
// Full data model per PROJECT_PLAN.md §3.
// Multi-tenancy rule: every query must be scoped by clinicId — enforced via
// the repository pattern in the API (services require clinicId on every call),
// with an explicit cross-tenant isolation test.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum AppointmentStatus {
pending
confirmed
cancelled
no_show
completed
}
enum AppointmentSource {
whatsapp
dashboard
}
enum MessageDirection {
inbound @map("in")
outbound @map("out")
}
enum ReminderKind {
h24 @map("24h")
h2 @map("2h")
}
enum ReminderStatus {
pending
sent
cancelled
failed
}
enum WaitlistStatus {
active
notified
fulfilled
cancelled
expired
}
enum SlotHoldStatus {
offered
accepted
declined
expired
cancelled
}
model Clinic {
id String @id @default(cuid())
name String
phone String // WhatsApp business number (display)
waPhoneNumberId String? @unique // Meta phone_number_id — routes inbound webhooks to the clinic
timezone String @default("Europe/Istanbul")
defaultLanguage String @default("tr")
workingHours Json @default("{}") // per-weekday intervals
settings Json @default("{}") // reminder offsets, deposit on/off, ...
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
staff Staff[]
services Service[]
patients Patient[]
appointments Appointment[]
waitlistEntries WaitlistEntry[]
conversations Conversation[]
auditLogs AuditLog[]
slotHolds SlotHold[]
}
// Owner accounts only for MVP (PROJECT_PLAN §2 Auth).
model User {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
email String @unique
passwordHash String
name String
role String @default("owner")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([clinicId])
}
model Staff {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
name String
role String @default("practitioner")
workingHours Json @default("{}")
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
services Service[] @relation("StaffServices")
appointments Appointment[]
@@index([clinicId])
}
model Service {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
name Json // i18n: { ar, tr, en }
durationMinutes Int
price Decimal? @db.Decimal(10, 2)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
staff Staff[] @relation("StaffServices")
appointments Appointment[]
waitlistEntries WaitlistEntry[]
@@index([clinicId])
}
model Patient {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
waPhone String
name String?
language String @default("tr")
notes String?
tags String[] @default([])
lastVisitAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
appointments Appointment[]
waitlistEntries WaitlistEntry[]
conversations Conversation[]
@@unique([clinicId, waPhone])
@@index([clinicId])
}
model Appointment {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
patientId String
patient Patient @relation(fields: [patientId], references: [id])
staffId String
staff Staff @relation(fields: [staffId], references: [id])
serviceId String
service Service @relation(fields: [serviceId], references: [id])
startsAt DateTime
endsAt DateTime
status AppointmentStatus @default(pending)
source AppointmentSource @default(whatsapp)
cancelReason String?
unconfirmedRisk Boolean @default(false) // no reply to reminders by T-4h
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reminderJobs ReminderJob[]
@@index([clinicId, startsAt])
@@index([clinicId, status])
@@index([staffId, startsAt])
}
model WaitlistEntry {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
patientId String
patient Patient @relation(fields: [patientId], references: [id])
serviceId String
service Service @relation(fields: [serviceId], references: [id])
preferredWindow Json // date ranges / times
status WaitlistStatus @default(active)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
slotHolds SlotHold[]
@@index([clinicId, status])
}
// A freed slot offered to one waitlisted patient at a time (Phase 5).
model SlotHold {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
waitlistEntryId String
waitlistEntry WaitlistEntry @relation(fields: [waitlistEntryId], references: [id], onDelete: Cascade)
staffId String
serviceId String
startsAt DateTime
endsAt DateTime
status SlotHoldStatus @default(offered)
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([clinicId, status])
@@index([staffId, startsAt])
}
model Conversation {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
patientId String
patient Patient @relation(fields: [patientId], references: [id])
state Json @default("{}") // agent memory / slots (compact facts, not transcripts)
lastMessageAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages Message[]
@@index([clinicId, patientId])
}
model Message {
id String @id @default(cuid())
conversationId String
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
direction MessageDirection
waMessageId String? @unique
type String // text | interactive | template | ...
body String?
payload Json?
createdAt DateTime @default(now())
@@index([conversationId, createdAt])
}
model ReminderJob {
id String @id @default(cuid())
appointmentId String
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
kind ReminderKind
scheduledFor DateTime
status ReminderStatus @default(pending)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([appointmentId])
@@index([status, scheduledFor])
}
model AuditLog {
id String @id @default(cuid())
clinicId String
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
actor String // "agent" | "owner:
" | "system"
action String
meta Json @default("{}")
createdAt DateTime @default(now())
@@index([clinicId, createdAt])
}