Files
DentalManagementMH07/packages/db/generated/prisma/schema.prisma
ff b1932fa3d6 feat: add Provider field to appointment form, drive AI claim automation
Adds an optional Provider (NPI) select to the Edit Appointment form,
sourced from Settings > NPI Providers. The AI "Claim for Column" batch
flow now prefers the appointment's own provider when resolving which
provider Selenium should select on the insurance site (e.g. MassHealth
rendering provider dropdown), falling back to the existing per-procedure
selection, active claim, or first configured provider.

Also resyncs packages/db's committed compiled schema/client artifacts
with their .ts sources — they had drifted out of sync (some untouched
since July), which was silently stripping the new npiProviderId field
via stale .strict() zod schemas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 23:07:10 -04:00

761 lines
21 KiB
Plaintext
Executable File

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
generator zod {
provider = "prisma-zod-generator"
output = "../shared/" // Zod schemas will be generated here inside `db/shared`
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
autoBackupEnabled Boolean @default(true)
autoBackupHour Int @default(20)
usbBackupEnabled Boolean @default(false)
usbBackupHour Int @default(21)
autoMhCheckEnabled Boolean @default(false)
autoMhCheckDayOfWeek Int @default(1)
autoMhCheckHour Int @default(13)
patients Patient[]
appointments Appointment[]
staff Staff[]
npiProviders NpiProvider[]
claims Claim[]
insuranceCredentials InsuranceCredential[]
shoppingVendors ShoppingVendor[]
updatedPayments Payment[] @relation("PaymentUpdatedBy")
backups DatabaseBackup[]
backupDestinations BackupDestination[]
notifications Notification[]
cloudFolders CloudFolder[]
cloudFiles CloudFile[]
communications Communication[]
twilioSettings TwilioSettings?
aiSettings AiSettings?
officeHours OfficeHours?
officeContact OfficeContact?
procedureTimeslot ProcedureTimeslot?
insuranceContacts InsuranceContact[]
patientConversations PatientConversation[]
labRxTemplates LabRxTemplate[]
seleniumSettings SeleniumSettings?
}
model Patient {
id Int @id @default(autoincrement())
firstName String
lastName String
dateOfBirth DateTime? @db.Date
gender String
phone String
email String?
address String?
city String?
zipCode String?
insuranceProvider String?
insuranceId String?
groupNumber String?
policyHolder String?
allergies String?
medicalConditions String?
preferredLanguage String? @default("English")
status PatientStatus @default(UNKNOWN)
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
appointments Appointment[]
procedures AppointmentProcedure[]
claims Claim[]
groups PdfGroup[]
payment Payment[]
communications Communication[]
documents PatientDocument[]
conversation PatientConversation?
cloudFolders CloudFolder[] @relation("PatientCloudFolder")
@@index([insuranceId])
@@index([createdAt])
}
enum PatientStatus {
ACTIVE
INACTIVE
UNKNOWN
PLAN_NOT_ACCEPTED
}
model Appointment {
id Int @id @default(autoincrement())
patientId Int
userId Int
staffId Int
npiProviderId Int?
title String
date DateTime @db.Date
startTime String // Store time as "hh:mm"
endTime String // Store time as "hh:mm"
type String // e.g., "checkup", "cleaning", "filling", etc.
typeLocked Boolean @default(false) // true = user manually set; auto-sync will not overwrite
notes String?
procedureCodeNotes String?
status String @default("scheduled") // "scheduled", "completed", "cancelled", "no-show"
movedByAi Boolean @default(false)
createdAt DateTime @default(now())
eligibilityStatus PatientStatus @default(UNKNOWN)
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
staff Staff? @relation(fields: [staffId], references: [id])
npiProvider NpiProvider? @relation(fields: [npiProviderId], references: [id])
procedures AppointmentProcedure[]
claims Claim[]
files AppointmentFile[]
@@index([patientId])
@@index([date])
}
model AppointmentFile {
id Int @id @default(autoincrement())
appointmentId Int
filename String
mimeType String?
filePath String?
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
@@index([appointmentId])
}
model Staff {
id Int @id @default(autoincrement())
userId Int
name String
email String?
role String // e.g., "Dentist", "Hygienist", "Assistant"
phone String?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
appointments Appointment[]
claims Claim[] @relation("ClaimStaff")
}
model NpiProvider {
id Int @id @default(autoincrement())
userId Int
npiNumber String
providerName String
sortOrder Int @default(0)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
claims Claim[]
payments Payment[]
commissionBatches CommissionBatch[]
appointmentProcedures AppointmentProcedure[]
appointments Appointment[]
@@unique([userId, npiNumber])
@@index([userId])
}
enum ProcedureSource {
COMBO
MANUAL
}
model AppointmentProcedure {
id Int @id @default(autoincrement())
appointmentId Int
patientId Int
npiProviderId Int?
procedureCode String
procedureLabel String?
fee Decimal? @db.Decimal(10, 2)
category String?
toothNumber String?
toothSurface String?
oralCavityArea String?
source ProcedureSource @default(MANUAL)
comboKey String?
createdAt DateTime @default(now())
appointment Appointment @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
npiProvider NpiProvider? @relation(fields: [npiProviderId], references: [id])
@@index([appointmentId])
@@index([patientId])
}
model Claim {
id Int @id @default(autoincrement())
patientId Int
/// @zod.number.int().nullable().optional()
appointmentId Int?
userId Int
staffId Int
patientName String
memberId String
dateOfBirth DateTime @db.Date
remarks String
missingTeethStatus MissingTeethStatus @default(No_missing)
missingTeeth Json? // { "T_14": "X", "T_G": "O", ... }
serviceDate DateTime
insuranceProvider String // e.g., "Delta MA"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
status ClaimStatus @default(PENDING)
claimNumber String?
preAuthNumber String?
npiProviderId Int?
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
appointment Appointment? @relation(fields: [appointmentId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id])
staff Staff? @relation("ClaimStaff", fields: [staffId], references: [id])
npiProvider NpiProvider? @relation(fields: [npiProviderId], references: [id])
serviceLines ServiceLine[]
claimFiles ClaimFile[]
payment Payment?
}
enum ClaimStatus {
PENDING
APPROVED
CANCELLED
REVIEW
VOID
PREAUTH
}
enum MissingTeethStatus {
No_missing
endentulous
Yes_missing
}
model ServiceLine {
id Int @id @default(autoincrement())
claimId Int?
paymentId Int?
procedureCode String
procedureDate DateTime @db.Date
quad String?
arch String?
toothNumber String?
toothSurface String?
icn String?
paidCode String?
allowedAmount Decimal? @db.Decimal(10, 2)
totalBilled Decimal @db.Decimal(10, 2)
totalPaid Decimal @default(0.00) @db.Decimal(10, 2)
totalAdjusted Decimal @default(0.00) @db.Decimal(10, 2)
totalDue Decimal @default(0.00) @db.Decimal(10, 2)
status ServiceLineStatus @default(UNPAID)
claim Claim? @relation(fields: [claimId], references: [id], onDelete: Cascade)
payment Payment? @relation(fields: [paymentId], references: [id], onDelete: Cascade)
serviceLineTransactions ServiceLineTransaction[]
}
enum ServiceLineStatus {
PENDING
PARTIALLY_PAID
PAID
UNPAID
ADJUSTED
OVERPAID
DENIED
}
model ClaimFile {
id Int @id @default(autoincrement())
claimId Int
filename String
mimeType String
filePath String?
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
}
model InsuranceCredential {
id Int @id @default(autoincrement())
userId Int
siteKey String
username String
password String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, siteKey])
@@index([userId])
}
model ShoppingVendor {
id Int @id @default(autoincrement())
userId Int
vendorName String
websiteUrl String
loginUsername String
loginPassword String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model PdfGroup {
id Int @id @default(autoincrement())
title String
titleKey PdfTitleKey @default(OTHER)
createdAt DateTime @default(now())
patientId Int
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
pdfs PdfFile[]
@@index([patientId])
@@index([titleKey])
}
model PdfFile {
id Int @id @default(autoincrement())
filename String
pdfData Bytes
uploadedAt DateTime @default(now())
groupId Int
group PdfGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
@@index([groupId])
}
enum PdfTitleKey {
INSURANCE_CLAIM
INSURANCE_CLAIM_PREAUTH
ELIGIBILITY_STATUS
CLAIM_STATUS
OTHER
}
model Payment {
id Int @id @default(autoincrement())
claimId Int? @unique
patientId Int
userId Int
updatedById Int?
npiProviderId Int?
totalBilled Decimal @db.Decimal(10, 2)
totalPaid Decimal @default(0.00) @db.Decimal(10, 2)
totalAdjusted Decimal @default(0.00) @db.Decimal(10, 2)
totalDue Decimal @db.Decimal(10, 2)
mhPaidAmount Decimal? @db.Decimal(10, 2)
copayment Decimal @default(0.00) @db.Decimal(10, 2)
adjustment Decimal @default(0.00) @db.Decimal(10, 2)
status PaymentStatus @default(PENDING)
notes String?
icn String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
claim Claim? @relation(fields: [claimId], references: [id], onDelete: Cascade)
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
updatedBy User? @relation("PaymentUpdatedBy", fields: [updatedById], references: [id])
npiProvider NpiProvider? @relation(fields: [npiProviderId], references: [id])
serviceLineTransactions ServiceLineTransaction[]
serviceLines ServiceLine[]
commissionBatchItems CommissionBatchItem[]
@@index([claimId])
@@index([patientId])
@@index([createdAt])
}
model ServiceLineTransaction {
id Int @id @default(autoincrement())
paymentId Int
serviceLineId Int
transactionId String?
paidAmount Decimal @db.Decimal(10, 2)
adjustedAmount Decimal @default(0.00) @db.Decimal(10, 2)
method PaymentMethod
receivedDate DateTime
payerName String?
notes String?
createdAt DateTime @default(now())
payment Payment @relation(fields: [paymentId], references: [id], onDelete: Cascade)
serviceLine ServiceLine @relation(fields: [serviceLineId], references: [id], onDelete: Cascade)
@@index([paymentId])
@@index([serviceLineId])
}
enum PaymentStatus {
PENDING
PARTIALLY_PAID
PAID
OVERPAID
DENIED
VOID
}
enum PaymentMethod {
EFT
CHECK
CASH
CARD
OTHER
}
// Database management page
model DatabaseBackup {
id Int @id @default(autoincrement())
userId Int
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([createdAt])
}
model BackupDestination {
id Int @id @default(autoincrement())
userId Int
path String
isActive Boolean @default(true)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
model Notification {
id Int @id @default(autoincrement())
userId Int
type NotificationTypes
message String
createdAt DateTime @default(now())
read Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([createdAt])
}
enum NotificationTypes {
BACKUP
CLAIM
PAYMENT
ETC
}
// Cron job execution log
model CronJobLog {
id Int @id @default(autoincrement())
jobName String // e.g. "local-backup", "usb-backup"
status String // "success" | "failed" | "skipped"
startedAt DateTime
completedAt DateTime?
durationMs Int?
errorMessage String?
@@index([jobName])
@@index([startedAt])
@@index([status])
}
model CloudFolder {
id Int @id @default(autoincrement())
userId Int
name String
parentId Int?
patientId Int?
parent CloudFolder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
children CloudFolder[] @relation("FolderChildren")
user User @relation(fields: [userId], references: [id])
patient Patient? @relation("PatientCloudFolder", fields: [patientId], references: [id], onDelete: SetNull)
files CloudFile[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, parentId, name]) // prevents sibling folder name duplicates
@@index([parentId])
@@index([patientId])
}
model CloudFile {
id Int @id @default(autoincrement())
userId Int
name String
mimeType String?
fileSize BigInt @db.BigInt
folderId Int? // optional: null => root
isComplete Boolean @default(false) // upload completed?
totalChunks Int? // optional: expected number of chunks
diskPath String? // relative path on disk under uploads/
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
folder CloudFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
chunks CloudFileChunk[]
@@index([folderId])
}
model CloudFileChunk {
id Int @id @default(autoincrement())
fileId Int
seq Int
data Bytes
createdAt DateTime @default(now())
file CloudFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
@@unique([fileId, seq])
@@index([fileId, seq])
}
// patient-connection-
enum CommunicationChannel {
sms
voice
}
enum CommunicationDirection {
outbound
inbound
}
enum CommunicationStatus {
queued
sent
delivered
failed
completed
busy
no_answer
}
model Communication {
id Int @id @default(autoincrement())
patientId Int
userId Int?
channel CommunicationChannel
direction CommunicationDirection
status CommunicationStatus
body String?
callDuration Int?
twilioSid String?
createdAt DateTime @default(now())
// Relations
patient Patient @relation(fields: [patientId], references: [id])
user User? @relation(fields: [userId], references: [id])
@@map("communications")
}
model PatientDocument {
id Int @id @default(autoincrement())
patientId Int
filename String
originalName String
mimeType String
fileSize BigInt
filePath String
uploadedAt DateTime @default(now())
updatedAt DateTime @updatedAt
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
@@index([patientId])
@@index([uploadedAt])
}
model TwilioSettings {
id Int @id @default(autoincrement())
userId Int @unique
accountSid String
authToken String
phoneNumber String
greetingMessage String?
templates Json?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("twilio_settings")
}
model AiSettings {
id Int @id @default(autoincrement())
userId Int @unique
apiKey String
aiEnabled Boolean @default(true)
openAiKey String @default("")
openAiEnabled Boolean @default(false)
claudeAiKey String @default("")
claudeAiEnabled Boolean @default(false)
claudeAiModel String @default("claude-haiku-4-5-20251001")
openAiModel String @default("gpt-5.2")
googleAiModel String @default("gemini-2.5-flash")
dentalMgmtKey String @default("")
dentalMgmtEnabled Boolean @default(false)
afterHoursEnabled Boolean @default(true)
openPhoneReply Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("ai_settings")
}
model OfficeHours {
id Int @id @default(autoincrement())
userId Int @unique
data Json
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("office_hours")
}
model OfficeContact {
id Int @id @default(autoincrement())
userId Int @unique
officeName String?
receptionistName String?
dentistName String?
phoneNumber String?
email String?
fax String?
streetAddress String?
city String?
state String?
zipCode String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("office_contact")
}
model InsuranceContact {
id Int @id @default(autoincrement())
userId Int
name String
phoneNumber String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("insurance_contact")
}
model ProcedureTimeslot {
id Int @id @default(autoincrement())
userId Int @unique
data Json
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("procedure_timeslot")
}
model PatientConversation {
id Int @id @default(autoincrement())
patientId Int @unique
userId Int
stage String @default("initial")
aiHandoff Boolean @default(true)
updatedAt DateTime @updatedAt
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("patient_conversation")
}
model LabRxTemplate {
id Int @id @default(autoincrement())
userId Int
name String
labName String?
labPhone String?
labFax String?
labAddress String?
labAccount String?
caseType String?
material String?
instructions String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("lab_rx_template")
}
// Commission tracking
model CommissionBatch {
id Int @id @default(autoincrement())
npiProviderId Int
totalCollection Decimal @db.Decimal(14, 2)
commissionAmount Decimal @db.Decimal(14, 2)
notes String?
createdAt DateTime @default(now())
npiProvider NpiProvider @relation(fields: [npiProviderId], references: [id])
items CommissionBatchItem[]
@@index([npiProviderId])
}
model CommissionBatchItem {
id Int @id @default(autoincrement())
commissionBatchId Int
paymentId Int
collectionAmount Decimal @db.Decimal(14, 2)
commissionBatch CommissionBatch @relation(fields: [commissionBatchId], references: [id], onDelete: Cascade)
payment Payment @relation(fields: [paymentId], references: [id])
@@unique([commissionBatchId, paymentId])
@@index([paymentId])
}
model SeleniumSettings {
id Int @id @default(autoincrement())
userId Int @unique
paymentGroupId String @default("")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("selenium_settings")
}