Initial commit
Next.js + Express event management app for Hope Family Church.
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
// 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])
|
||||
// }
|
||||
Reference in New Issue
Block a user