feat: full clinera platform — auth, appointments, WhatsApp, chatbot canvas, reminders, leads, campaigns, inbox
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,114 @@
|
||||
-- AI-Powered Tree-Based Reminder Sequence System
|
||||
-- This migration adds the foundation for sophisticated reminder trees
|
||||
|
||||
-- Core Tree Definition
|
||||
CREATE TABLE "ReminderTree" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"clinicId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"triggerType" TEXT NOT NULL, -- booking_created | no_show | completed | custom
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"metadata" TEXT, -- JSON: A/B test groups, analytics tags, etc.
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
FOREIGN KEY ("clinicId") REFERENCES "Clinic" ("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Individual nodes in the tree
|
||||
CREATE TABLE "TreeNode" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"treeId" TEXT NOT NULL,
|
||||
"parentId" TEXT, -- NULL for root node
|
||||
"name" TEXT NOT NULL,
|
||||
"nodeType" TEXT NOT NULL, -- wait | send_message | send_buttons | condition | action | collect_input
|
||||
"content" TEXT, -- Message template or condition logic
|
||||
"offsetMinutes" INTEGER DEFAULT 0, -- Delay from parent execution
|
||||
"conditions" TEXT, -- JSON: Complex conditions for routing
|
||||
"metadata" TEXT, -- JSON: Additional node configuration
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("treeId") REFERENCES "ReminderTree" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("parentId") REFERENCES "TreeNode" ("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Track user journey through trees
|
||||
CREATE TABLE "UserJourney" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"appointmentId" TEXT NOT NULL,
|
||||
"treeId" TEXT NOT NULL,
|
||||
"currentNodeId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT "active", -- active | completed | paused | failed
|
||||
"journeyData" TEXT, -- JSON: Collected responses, scores, etc.
|
||||
"startedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastActiveAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" DATETIME,
|
||||
FOREIGN KEY ("appointmentId") REFERENCES "Appointment" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("treeId") REFERENCES "ReminderTree" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("currentNodeId") REFERENCES "TreeNode" ("id") ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Track execution of individual tree nodes
|
||||
CREATE TABLE "NodeExecution" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"journeyId" TEXT NOT NULL,
|
||||
"nodeId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT "pending", -- pending | executed | failed | skipped
|
||||
"executedAt" DATETIME,
|
||||
"response" TEXT, -- User response to this node
|
||||
"metadata" TEXT, -- JSON: Execution details, errors, etc.
|
||||
"nextNodeId" TEXT, -- Which node was selected as next
|
||||
FOREIGN KEY ("journeyId") REFERENCES "UserJourney" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("nodeId") REFERENCES "TreeNode" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("nextNodeId") REFERENCES "TreeNode" ("id") ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Patient behavioral scoring system
|
||||
CREATE TABLE "PatientProfile" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"leadId" TEXT NOT NULL,
|
||||
"clinicId" TEXT NOT NULL,
|
||||
"responseStyle" TEXT NOT NULL DEFAULT "standard", -- quick | detailed | minimal | emoji_heavy
|
||||
"preferredTime" TEXT, -- JSON: Hour ranges when they respond most
|
||||
"engagementScore" REAL NOT NULL DEFAULT 50.0, -- 0-100 based on interaction quality
|
||||
"noShowRisk" REAL NOT NULL DEFAULT 50.0, -- 0-100 predicted likelihood
|
||||
"conversationTone" TEXT NOT NULL DEFAULT "neutral", -- formal | casual | friendly | professional
|
||||
"languagePreference" TEXT NOT NULL DEFAULT "auto", -- auto | english | arabic
|
||||
"lastInteractionAt" DATETIME,
|
||||
"profileData" TEXT, -- JSON: Detailed behavioral analytics
|
||||
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("leadId") REFERENCES "Lead" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("clinicId") REFERENCES "Clinic" ("id") ON DELETE CASCADE,
|
||||
UNIQUE("leadId", "clinicId")
|
||||
);
|
||||
|
||||
-- Smart A/B testing for different tree paths
|
||||
CREATE TABLE "TreeExperiment" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"clinicId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"controlTreeId" TEXT NOT NULL,
|
||||
"variantTreeId" TEXT NOT NULL,
|
||||
"trafficSplit" REAL NOT NULL DEFAULT 50.0, -- Percentage for variant
|
||||
"status" TEXT NOT NULL DEFAULT "draft", -- draft | running | paused | completed
|
||||
"startDate" DATETIME,
|
||||
"endDate" DATETIME,
|
||||
"metrics" TEXT, -- JSON: Conversion goals, success criteria
|
||||
"results" TEXT, -- JSON: Experiment results
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("clinicId") REFERENCES "Clinic" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("controlTreeId") REFERENCES "ReminderTree" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("variantTreeId") REFERENCES "ReminderTree" ("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX "idx_reminder_tree_clinic" ON "ReminderTree"("clinicId", "isActive");
|
||||
CREATE INDEX "idx_tree_node_tree" ON "TreeNode"("treeId", "sortOrder");
|
||||
CREATE INDEX "idx_tree_node_parent" ON "TreeNode"("parentId");
|
||||
CREATE INDEX "idx_user_journey_appointment" ON "UserJourney"("appointmentId", "status");
|
||||
CREATE INDEX "idx_user_journey_current" ON "UserJourney"("currentNodeId", "lastActiveAt");
|
||||
CREATE INDEX "idx_node_execution_journey" ON "NodeExecution"("journeyId", "executedAt");
|
||||
CREATE INDEX "idx_patient_profile_lead" ON "PatientProfile"("leadId");
|
||||
CREATE INDEX "idx_tree_experiment_clinic" ON "TreeExperiment"("clinicId", "status");
|
||||
@@ -0,0 +1,378 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Clinic {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
email String @unique
|
||||
passwordHash String?
|
||||
phone String?
|
||||
clinicType String @default("Medical Clinic")
|
||||
wahaSessionId String?
|
||||
wahaStatus String @default("disconnected")
|
||||
wahaLastChecked DateTime?
|
||||
timezone String @default("Asia/Riyadh")
|
||||
quietHoursStart Int @default(22)
|
||||
quietHoursEnd Int @default(8)
|
||||
plan String @default("free")
|
||||
planExpiresAt DateTime?
|
||||
trialEndsAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sessions AppSession[]
|
||||
appointments Appointment[]
|
||||
auditLogs AuditLog[]
|
||||
cronLogs CronLog[]
|
||||
leads Lead[]
|
||||
patientNotes PatientNote[]
|
||||
messages MessageLog[]
|
||||
oauthAccounts OAuthAccount[]
|
||||
patientProfiles PatientProfile[]
|
||||
reminders ReminderTemplate[]
|
||||
reminderTrees ReminderTree[]
|
||||
treatments Treatment[]
|
||||
treeExperiments TreeExperiment[]
|
||||
campaigns Campaign[]
|
||||
staff Staff[]
|
||||
}
|
||||
|
||||
model OAuthAccount {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
provider String
|
||||
providerId String
|
||||
email String?
|
||||
name String?
|
||||
avatarUrl String?
|
||||
accessToken String?
|
||||
refreshToken String?
|
||||
expiresAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerId])
|
||||
@@index([clinicId])
|
||||
@@index([provider, email])
|
||||
}
|
||||
|
||||
model AppSession {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
token String @unique
|
||||
userAgent String?
|
||||
ipAddress String?
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
lastSeenAt DateTime @default(now())
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([clinicId])
|
||||
@@index([token])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
action String
|
||||
actor String?
|
||||
resource String?
|
||||
metadata String?
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
createdAt DateTime @default(now())
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([clinicId, createdAt])
|
||||
@@index([action, createdAt])
|
||||
}
|
||||
|
||||
model Treatment {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
name String
|
||||
duration Int @default(30)
|
||||
price Float?
|
||||
prepInstructions String?
|
||||
aftercareInstructions String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
appointments Appointment[]
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Lead {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
name String
|
||||
phone String
|
||||
email String?
|
||||
source String @default("manual")
|
||||
status String @default("new")
|
||||
treatmentInterest String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
appointments Appointment[]
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
patientNotes PatientNote[]
|
||||
patientProfiles PatientProfile[]
|
||||
}
|
||||
|
||||
model Appointment {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
leadId String
|
||||
treatmentId String
|
||||
dateTime DateTime
|
||||
status String @default("scheduled")
|
||||
confirmToken String?
|
||||
confirmedAt DateTime?
|
||||
arrivedAt DateTime?
|
||||
completedAt DateTime?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
treatment Treatment @relation(fields: [treatmentId], references: [id], onDelete: Cascade)
|
||||
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
messages MessageLog[]
|
||||
scheduledReminders ScheduledReminder[]
|
||||
userJourneys UserJourney[]
|
||||
}
|
||||
|
||||
model ReminderTemplate {
|
||||
id String @id @default(cuid())
|
||||
clinicId String?
|
||||
trigger String
|
||||
offsetMinutes Int
|
||||
message String
|
||||
isActive Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
clinic Clinic? @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
scheduledReminders ScheduledReminder[]
|
||||
}
|
||||
|
||||
model ScheduledReminder {
|
||||
id String @id @default(cuid())
|
||||
appointmentId String
|
||||
templateId String
|
||||
scheduledFor DateTime
|
||||
status String @default("pending")
|
||||
sentAt DateTime?
|
||||
failureReason String?
|
||||
retryCount Int @default(0)
|
||||
nextRetryAt DateTime?
|
||||
template ReminderTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([status, scheduledFor])
|
||||
}
|
||||
|
||||
model MessageLog {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
appointmentId String?
|
||||
phone String
|
||||
direction String @default("outgoing")
|
||||
message String
|
||||
status String @default("queued")
|
||||
wahaMessageId String?
|
||||
sentAt DateTime?
|
||||
deliveredAt DateTime?
|
||||
readAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
appointment Appointment? @relation(fields: [appointmentId], references: [id])
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([wahaMessageId])
|
||||
}
|
||||
|
||||
model CronLog {
|
||||
id String @id @default(cuid())
|
||||
clinicId String?
|
||||
type String
|
||||
sent Int @default(0)
|
||||
skipped Int @default(0)
|
||||
failed Int @default(0)
|
||||
retried Int @default(0)
|
||||
durationMs Int @default(0)
|
||||
error String?
|
||||
ranAt DateTime @default(now())
|
||||
clinic Clinic? @relation(fields: [clinicId], references: [id])
|
||||
|
||||
@@index([type, ranAt])
|
||||
}
|
||||
|
||||
model ReminderTree {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
name String
|
||||
description String?
|
||||
triggerType String
|
||||
isActive Boolean @default(true)
|
||||
version Int @default(1)
|
||||
metadata String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
variantFor TreeExperiment[] @relation("VariantTree")
|
||||
controlFor TreeExperiment[] @relation("ControlTree")
|
||||
nodes TreeNode[]
|
||||
userJourneys UserJourney[]
|
||||
|
||||
@@index([clinicId, isActive])
|
||||
}
|
||||
|
||||
model TreeNode {
|
||||
id String @id @default(cuid())
|
||||
treeId String
|
||||
parentId String?
|
||||
name String
|
||||
nodeType String
|
||||
content String?
|
||||
offsetMinutes Int @default(0)
|
||||
conditions String?
|
||||
metadata String?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
nextNodes NodeExecution[] @relation("NextNode")
|
||||
executions NodeExecution[]
|
||||
parent TreeNode? @relation("TreeNodeParent", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children TreeNode[] @relation("TreeNodeParent")
|
||||
tree ReminderTree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
||||
userJourneys UserJourney[]
|
||||
|
||||
@@index([treeId, sortOrder])
|
||||
@@index([parentId])
|
||||
}
|
||||
|
||||
model UserJourney {
|
||||
id String @id @default(cuid())
|
||||
appointmentId String
|
||||
treeId String
|
||||
currentNodeId String?
|
||||
status String @default("active")
|
||||
journeyData String?
|
||||
startedAt DateTime @default(now())
|
||||
lastActiveAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
executions NodeExecution[]
|
||||
currentNode TreeNode? @relation(fields: [currentNodeId], references: [id])
|
||||
tree ReminderTree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
||||
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([appointmentId, status])
|
||||
@@index([currentNodeId, lastActiveAt])
|
||||
}
|
||||
|
||||
model NodeExecution {
|
||||
id String @id @default(cuid())
|
||||
journeyId String
|
||||
nodeId String
|
||||
status String @default("pending")
|
||||
executedAt DateTime?
|
||||
response String?
|
||||
metadata String?
|
||||
nextNodeId String?
|
||||
nextNode TreeNode? @relation("NextNode", fields: [nextNodeId], references: [id])
|
||||
node TreeNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||||
journey UserJourney @relation(fields: [journeyId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([journeyId, executedAt])
|
||||
}
|
||||
|
||||
model PatientProfile {
|
||||
id String @id @default(cuid())
|
||||
leadId String
|
||||
clinicId String
|
||||
responseStyle String @default("standard")
|
||||
preferredTime String?
|
||||
engagementScore Float @default(50.0)
|
||||
noShowRisk Float @default(50.0)
|
||||
conversationTone String @default("neutral")
|
||||
languagePreference String @default("auto")
|
||||
lastInteractionAt DateTime?
|
||||
profileData String?
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([leadId, clinicId])
|
||||
@@index([leadId])
|
||||
}
|
||||
|
||||
model TreeExperiment {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
name String
|
||||
description String?
|
||||
controlTreeId String
|
||||
variantTreeId String
|
||||
trafficSplit Float @default(50.0)
|
||||
status String @default("draft")
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
metrics String?
|
||||
results String?
|
||||
createdAt DateTime @default(now())
|
||||
variantTree ReminderTree @relation("VariantTree", fields: [variantTreeId], references: [id], onDelete: Cascade)
|
||||
controlTree ReminderTree @relation("ControlTree", fields: [controlTreeId], references: [id], onDelete: Cascade)
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([clinicId, status])
|
||||
}
|
||||
|
||||
model PatientNote {
|
||||
id String @id @default(cuid())
|
||||
leadId String
|
||||
clinicId String
|
||||
type String @default("note")
|
||||
content String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([leadId])
|
||||
}
|
||||
|
||||
model Campaign {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
name String
|
||||
message String
|
||||
targetType String
|
||||
targetValue String?
|
||||
sentCount Int @default(0)
|
||||
status String @default("sent")
|
||||
createdAt DateTime @default(now())
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([clinicId, createdAt])
|
||||
}
|
||||
|
||||
model Staff {
|
||||
id String @id @default(cuid())
|
||||
clinicId String
|
||||
name String
|
||||
email String
|
||||
passwordHash String
|
||||
role String @default("receptionist")
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
clinic Clinic @relation(fields: [clinicId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([clinicId, email])
|
||||
@@index([clinicId])
|
||||
}
|
||||
Reference in New Issue
Block a user