// 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]) // }