Files
hope-events/backend/prisma/schema.prisma
T
joshua 3d381944d2 Initial commit
Next.js + Express event management app for Hope Family Church.
2026-07-23 15:26:47 +02:00

637 lines
21 KiB
Plaintext

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
admin
supervisor
staff
user
}
enum NotificationPreference {
email
whatsapp
both
}
enum RegistrationStatus {
pending
confirmed
partial_paid
paid
cancelled
}
enum CashupStatus {
open
closed
}
enum CashupAction {
closed
quick_closed
reopened
}
enum EventCostType {
once_off
per_item
}
model User {
id String @id @default(uuid())
name String
email String @unique
password String
createdAt DateTime @default(now())
isActive Boolean @default(true)
phoneNumber String? @unique
updatedAt DateTime @updatedAt
role UserRole @default(user)
failedAttempts Int @default(0)
lockUntil DateTime?
tokenVersion Int @default(0)
notificationPreference NotificationPreference @default(email)
registrations Registration[]
payments Payment[]
tickets Ticket[]
ticketScans TicketUsage[]
passwordResets PasswordReset[]
Event Event[]
FormDraft FormDraft[]
eventsClosed Event[] @relation("EventClosedBy")
eventsReopened Event[] @relation("EventReopenedBy")
cashupsPerformed EventCashup[]
notifyForEvents Event[] @relation("EventNotifyRecipients")
}
model Event {
id String @id @default(uuid())
title String
description String?
startDate DateTime
endDate DateTime
registrationDeadline DateTime?
goLiveAt DateTime @default(now())
price Float
picture String?
isActive Boolean @default(true)
isHidden Boolean @default(false)
requiresAuth Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdById String?
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
redirectUrl String? @unique
eventOptions EventOption[]
registrations Registration[]
payments Payment[]
tickets Ticket[]
attachments EventAttachment[]
form EventForm?
Section Section[]
cashupStatus CashupStatus @default(open)
cashupDraft Json?
closedAt DateTime?
closedById String?
closedBy User? @relation("EventClosedBy", fields: [closedById], references: [id], onDelete: SetNull)
reopenedAt DateTime?
reopenedById String?
reopenedBy User? @relation("EventReopenedBy", fields: [reopenedById], references: [id], onDelete: SetNull)
costs EventCost[]
cashups EventCashup[]
// Users who should receive registration/payment/daily-summary notifications for this
// event. Falls back to `createdBy` when empty (see backend/src/utils/notifications.js).
notifyRecipients User[] @relation("EventNotifyRecipients")
@@index([createdById])
@@index([closedById])
@@index([reopenedById])
}
model EventOption {
id String @id @default(uuid())
eventId String
name String
price Float
isMainTicket Boolean
stockLimit Int @default(0)
event Event @relation(fields: [eventId], references: [id], onDelete: Restrict)
registrationOptions RegistrationOption[]
earlyBirdTiers EarlyBirdTier[]
SectionOption SectionOption[]
variants OptionVariant[]
costs EventCost[]
@@index([eventId])
}
model EarlyBirdTier {
id String @id @default(uuid())
eventOptionId String
variantId String?
deadline DateTime
price Float
order Int @default(0)
stockLimit Int @default(0)
eventOption EventOption @relation(fields: [eventOptionId], references: [id], onDelete: Cascade)
variant OptionVariant? @relation(fields: [variantId], references: [id], onDelete: Cascade)
registrationOptions RegistrationOption[]
@@index([eventOptionId, deadline])
@@index([variantId])
}
model OptionVariant {
id String @id @default(uuid())
eventOptionId String
name String
price Float?
stockLimit Int @default(0)
order Int @default(0)
eventOption EventOption @relation(fields: [eventOptionId], references: [id], onDelete: Cascade)
registrationOptions RegistrationOption[]
earlyBirdTiers EarlyBirdTier[]
@@index([eventOptionId])
}
model Registration {
id String @id @default(uuid())
userId String
eventId String
status RegistrationStatus @default(pending)
checkoutId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
event Event @relation(fields: [eventId], references: [id], onDelete: Restrict)
registrationOptions RegistrationOption[]
payments Payment[]
formResponses FormResponse[]
formDrafts FormDraft[]
@@index([userId])
@@index([eventId])
@@index([userId, status])
@@index([eventId, status])
}
model RegistrationOption {
id String @id @default(uuid())
registrationId String
eventOptionId String
quantity Int @default(1)
variantId String?
appliedTierId String?
priceSnapshot Float?
registration Registration @relation(fields: [registrationId], references: [id], onDelete: Restrict)
eventOption EventOption @relation(fields: [eventOptionId], references: [id], onDelete: Restrict)
variant OptionVariant? @relation(fields: [variantId], references: [id], onDelete: SetNull)
appliedTier EarlyBirdTier? @relation(fields: [appliedTierId], references: [id], onDelete: SetNull)
tickets Ticket[]
@@index([registrationId])
@@index([eventOptionId])
@@index([variantId])
@@index([appliedTierId])
}
model Payment {
id String @id @default(uuid())
amount Float
method String
userId String
registrationId String?
eventId String?
isDonation Boolean @default(false)
externalId String? @unique
status String?
originalPaymentId String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
registration Registration? @relation(fields: [registrationId], references: [id], onDelete: SetNull)
event Event? @relation(fields: [eventId], references: [id], onDelete: SetNull)
originalPayment Payment? @relation("SplitPayments", fields: [originalPaymentId], references: [id], onDelete: SetNull)
splitPayments Payment[] @relation("SplitPayments")
YocoTransaction YocoTransaction[]
@@index([userId])
@@index([registrationId])
@@index([eventId])
@@index([createdAt])
}
model Ticket {
id String @id @default(uuid())
qrCode String @unique
registrationOptionId String
userId String
eventId String
quantity Int @default(1)
isUsed Boolean @default(false)
emailSent Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
registrationOption RegistrationOption @relation(fields: [registrationOptionId], references: [id], onDelete: Restrict)
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
event Event @relation(fields: [eventId], references: [id], onDelete: Restrict)
usages TicketUsage[]
@@index([eventId])
@@index([userId])
@@index([registrationOptionId])
}
model TicketUsage {
id String @id @default(uuid())
ticketId String
scannedById String
scannedAt DateTime @default(now())
quantityRedeemed Int @default(1)
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Restrict)
scannedBy User @relation(fields: [scannedById], references: [id], onDelete: Restrict)
@@index([ticketId])
@@index([scannedAt])
@@index([scannedById, scannedAt])
}
model PasswordReset {
id String @id @default(uuid())
userId String
token String @unique
expiresAt DateTime
used Boolean @default(false)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model EventAttachment {
id String @id @default(uuid())
eventId String
originalName String
filename String
mimeType String
size Int
url String
createdAt DateTime @default(now())
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
@@index([eventId])
}
// Event registration forms
enum FormFieldType {
yes_no
text
date
numeric
statement
paragraph
}
model EventForm {
id String @id @default(uuid())
eventId String @unique
isRequired Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
fields EventFormField[]
}
model EventFormField {
id String @id @default(uuid())
formId String
type FormFieldType
label String
isRequired Boolean @default(false)
order Int @default(0)
helpText String?
options Json?
form EventForm @relation(fields: [formId], references: [id], onDelete: Cascade)
answers FormAnswer[]
@@index([formId])
}
model FormResponse {
id String @id @default(uuid())
registrationId String
createdAt DateTime @default(now())
registration Registration @relation(fields: [registrationId], references: [id], onDelete: Cascade)
answers FormAnswer[]
@@index([registrationId])
}
model FormAnswer {
id String @id @default(uuid())
responseId String
fieldId String
value String
response FormResponse @relation(fields: [responseId], references: [id], onDelete: Cascade)
field EventFormField @relation(fields: [fieldId], references: [id], onDelete: Cascade)
@@index([responseId])
@@index([fieldId])
}
// Draft storage for in-progress attendee forms
model FormDraft {
id String @id @default(uuid())
registrationId String
userId String
data Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
registration Registration @relation(fields: [registrationId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([registrationId, userId])
}
// Stores all webhook events from Yoco. If a Payment is created (registration or donation),
// the record is marked reconciled and linked via paymentId. Otherwise, remains unreconciled.
model YocoTransaction {
id String @id @default(uuid())
externalId String @unique // Yoco payment/event id
amount Int?
currency String?
createdDate DateTime?
checkoutId String?
methodType String? // paymentMethodDetails.type (e.g., card)
reconciled Boolean @default(false)
ignored Boolean @default(false)
paymentId String?
payment Payment? @relation(fields: [paymentId], references: [id], onDelete: SetNull)
raw Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([paymentId])
}
model Section {
id String @id @default(uuid())
eventId String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
allowedOptions SectionOption[]
@@index([eventId])
}
model SectionOption {
id String @id @default(uuid())
sectionId String
eventOptionId String
section Section @relation(fields: [sectionId], references: [id], onDelete: Cascade)
eventOption EventOption @relation(fields: [eventOptionId], references: [id], onDelete: Cascade)
@@unique([sectionId, eventOptionId])
}
model AppSetting {
key String @id
value String
updatedAt DateTime @updatedAt
}
model EventCost {
id String @id @default(uuid())
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
eventOptionId String?
eventOption EventOption? @relation(fields: [eventOptionId], references: [id], onDelete: Cascade)
label String
costType EventCostType
amount Float
paidFromMethod String? // 'cash' | 'card' | 'eft' | 'other' | null — null = not paid from event takings
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([eventId])
@@index([eventOptionId])
}
// Append-only audit trail: one row per close / quick-close / reopen. Never updated or deleted.
model EventCashup {
id String @id @default(uuid())
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
action CashupAction
unallocatedDonationsTotal Float @default(0)
totalCosts Float @default(0)
totalExpectedRevenue Float @default(0)
totalActualRevenue Float?
notes String?
performedById String?
performedBy User? @relation(fields: [performedById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
lines EventCashupLine[]
@@index([eventId, createdAt])
}
model EventCashupLine {
id String @id @default(uuid())
cashupId String
cashup EventCashup @relation(fields: [cashupId], references: [id], onDelete: Cascade)
method String
expectedAmount Float @default(0)
actualAmount Float?
variance Float?
notes String?
denominations EventCashupDenomination[]
@@index([cashupId])
}
model EventCashupDenomination {
id String @id @default(uuid())
lineId String
line EventCashupLine @relation(fields: [lineId], references: [id], onDelete: Cascade)
value Float
count Int
@@index([lineId])
}
// ─── Church Website Models (disabled — kept for reference, not active Prisma models) ──
//
// model Ministry {
// id String @id @default(uuid())
// slug String @unique
// title String
// description String?
// image String?
// isActive Boolean @default(true)
// order Int @default(0)
// leaderId String?
// leader TeamMember? @relation(fields: [leaderId], references: [id], onDelete: SetNull)
// interests MinistryInterest[]
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
//
// @@index([isActive, order])
// @@index([leaderId])
// }
//
// model TeamMember {
// id String @id @default(uuid())
// name String
// title String
// bio String?
// photo String?
// order Int @default(0)
// isActive Boolean @default(true)
// email String?
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// ministries Ministry[]
//
// @@index([isActive, order])
// }
//
// model ContentBlock {
// id String @id @default(uuid())
// page String
// key String
// value String
// type String @default("text")
// updatedAt DateTime @updatedAt
//
// @@unique([page, key])
// @@index([page])
// }
//
// model ChurchDocument {
// id String @id @default(uuid())
// title String
// description String?
// fileUrl String
// category String @default("other")
// isPublic Boolean @default(true)
// uploadedAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// }
//
// model FellowshipGroup {
// id String @id @default(uuid())
// name String
// description String?
// leader String?
// isActive Boolean @default(true)
// order Int @default(0)
// members ChurchMember[]
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
//
// @@index([isActive, order])
// }
//
// model ChurchMember {
// id String @id @default(uuid())
// status String @default("pending")
// mainName String
// mainEmail String
// mainPhone String?
// mainBirthday DateTime?
// mainOccupation String?
// mainEmploymentStatus String?
// mainDateOfSalvation DateTime?
// mainDateOfWaterBaptism DateTime?
// mainHolySpiritBaptism Boolean @default(false)
// spouseName String?
// spouseEmail String?
// spousePhone String?
// spouseBirthday DateTime?
// spouseOccupation String?
// spouseEmploymentStatus String?
// spouseDateOfSalvation DateTime?
// spouseDateOfWaterBaptism DateTime?
// spouseHolySpiritBaptism Boolean?
// homeAddress String?
// inFellowshipGroup Boolean @default(false)
// fellowshipGroupId String?
// fellowshipGroup FellowshipGroup? @relation(fields: [fellowshipGroupId], references: [id], onDelete: SetNull)
// membershipConfirmed Boolean @default(false)
// notes String?
// appliedAt DateTime @default(now())
// approvedAt DateTime?
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
// children ChurchChild[]
// ministryInterests MinistryInterest[]
// donations Donation[]
//
// @@index([status])
// @@index([mainEmail])
// @@index([fellowshipGroupId])
// }
//
// model ChurchChild {
// id String @id @default(uuid())
// memberId String
// name String
// birthday DateTime?
// member ChurchMember @relation(fields: [memberId], references: [id], onDelete: Cascade)
//
// @@index([memberId])
// }
//
// model MinistryInterest {
// id String @id @default(uuid())
// memberId String
// ministryId String
// forSpouse Boolean @default(false)
// member ChurchMember @relation(fields: [memberId], references: [id], onDelete: Cascade)
// ministry Ministry @relation(fields: [ministryId], references: [id], onDelete: Cascade)
//
// @@unique([memberId, ministryId, forSpouse])
// @@index([memberId])
// @@index([ministryId])
// }
//
// model Donation {
// id String @id @default(uuid())
// donorName String?
// donorEmail String?
// donorPhone String?
// amount Float
// method String
// reference String?
// note String?
// isAnonymous Boolean @default(false)
// linkedMemberId String?
// linkedMember ChurchMember? @relation(fields: [linkedMemberId], references: [id], onDelete: SetNull)
// createdAt DateTime @default(now())
// updatedAt DateTime @updatedAt
//
// @@index([linkedMemberId])
// @@index([createdAt])
// @@index([method])
// }