3d381944d2
Next.js + Express event management app for Hope Family Church.
637 lines
21 KiB
Plaintext
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])
|
|
// }
|