Files
calvana/pledge-now-pay-later/prisma/schema.prisma
Omair Saleh 0e8df76f89 fundraiser mode: external platforms, role-aware onboarding, show-don't-gate
SCHEMA:
- Organization.orgType: 'charity' | 'fundraiser'
- Organization.whatsappConnected: boolean
- Event.paymentMode: 'self' (bank transfer) | 'external' (redirect to URL)
- Event.externalUrl: fundraising page URL
- Event.externalPlatform: launchgood, enthuse, justgiving, gofundme, other

ONBOARDING (role-aware):
- Dashboard shows getting-started banner AT TOP, not full-page blocker
- First-time users see role picker: 'Charity/Mosque' vs 'Personal Fundraiser'
- POST /api/onboarding sets orgType
- Charity checklist: bank details → WhatsApp → create fundraiser → share link
- Fundraiser checklist: add fundraising page → WhatsApp → share pledge link → first pledge
- WhatsApp is now a core onboarding step for both types
- Banner is dismissable via X button
- Dashboard always shows stats (with zeros), progress bar, empty-state card

SHOW DON'T GATE:
- Stats cards show immediately (with zeros, slightly faded)
- Collection progress bar always visible
- Empty-state card says 'Your pledge data will appear here'
- Getting started is a guidance banner, not a lock screen

EXTERNAL PAYMENT FLOW:
- Events can be paymentMode='external' with externalUrl
- Pledge flow: amount → identity → 'Donate on LaunchGood' redirect (skips schedule + payment method)
- ExternalRedirectStep: branded per platform (LaunchGood green, Enthuse purple, etc.)
- Marks pledge as 'initiated' when donor clicks through
- WhatsApp sends donation link instead of bank details
- Share button shares the external URL

EVENT CREATION:
- Payment mode toggle: 'Bank transfer' vs 'External page'
- External shows URL input + platform dropdown
- Fundraiser orgs default to external mode
- Platform badge on event cards

PLATFORMS SUPPORTED:
🌙 LaunchGood, 💜 Enthuse, 💛 JustGiving, 💚 GoFundMe, 🔗 Other/Custom
2026-03-03 06:42:11 +08:00

223 lines
7.0 KiB
Plaintext

generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
orgType String @default("charity") // charity | fundraiser
country String @default("UK")
timezone String @default("Europe/London")
bankName String?
bankSortCode String?
bankAccountNo String?
bankAccountName String?
refPrefix String @default("PNPL")
logo String?
primaryColor String @default("#1e40af")
gcAccessToken String?
gcEnvironment String @default("sandbox")
whatsappConnected Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
events Event[]
pledges Pledge[]
imports Import[]
@@index([slug])
}
model User {
id String @id @default(cuid())
email String @unique
name String?
hashedPassword String?
role String @default("staff") // super_admin, org_admin, staff, volunteer
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
}
model Event {
id String @id @default(cuid())
name String
slug String
description String?
eventDate DateTime?
location String?
goalAmount Int? // in pence
currency String @default("GBP")
status String @default("active") // draft, active, closed, archived
paymentMode String @default("self") // self = we show bank details, external = redirect to URL
externalUrl String? // e.g. https://launchgood.com/my-campaign
externalPlatform String? // launchgood, enthuse, justgiving, gofundme, other
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
qrSources QrSource[]
pledges Pledge[]
@@unique([organizationId, slug])
@@index([organizationId, status])
}
model QrSource {
id String @id @default(cuid())
label String // "Table 5", "Volunteer: Ahmed"
code String @unique // short token for URL
volunteerName String?
tableName String?
eventId String
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
scanCount Int @default(0)
createdAt DateTime @default(now())
pledges Pledge[]
@@index([eventId])
@@index([code])
}
model Pledge {
id String @id @default(cuid())
reference String @unique // human-safe bank ref e.g. "PNPL-7K4P-50"
amountPence Int
currency String @default("GBP")
rail String // bank, gocardless, card
status String @default("new") // new, initiated, paid, overdue, cancelled
donorName String?
donorEmail String?
donorPhone String?
giftAid Boolean @default(false)
iPaidClickedAt DateTime?
notes String?
// Payment scheduling — the core of "pledge now, pay later"
dueDate DateTime? // null = pay now, set = promise to pay on this date
planId String? // groups installments together
installmentNumber Int? // e.g. 1 (of 4)
installmentTotal Int? // e.g. 4
reminderSentForDueDate Boolean @default(false)
eventId String
event Event @relation(fields: [eventId], references: [id])
qrSourceId String?
qrSource QrSource? @relation(fields: [qrSourceId], references: [id])
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
paymentInstruction PaymentInstruction?
payments Payment[]
reminders Reminder[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
paidAt DateTime?
cancelledAt DateTime?
@@index([organizationId, status])
@@index([reference])
@@index([eventId, status])
@@index([donorEmail])
@@index([donorPhone])
@@index([dueDate, status])
@@index([planId])
}
model PaymentInstruction {
id String @id @default(cuid())
pledgeId String @unique
pledge Pledge @relation(fields: [pledgeId], references: [id], onDelete: Cascade)
bankReference String // the unique ref to use
bankDetails Json // {sortCode, accountNo, accountName, bankName}
gcMandateId String?
gcMandateUrl String?
sentAt DateTime?
createdAt DateTime @default(now())
@@index([bankReference])
}
model Payment {
id String @id @default(cuid())
pledgeId String
pledge Pledge @relation(fields: [pledgeId], references: [id], onDelete: Cascade)
provider String // bank, gocardless, stripe
providerRef String? // external ID
amountPence Int
status String @default("pending") // pending, confirmed, failed
matchedBy String? // auto, manual
receivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
importId String?
import Import? @relation(fields: [importId], references: [id])
@@index([pledgeId])
@@index([providerRef])
}
model Reminder {
id String @id @default(cuid())
pledgeId String
pledge Pledge @relation(fields: [pledgeId], references: [id], onDelete: Cascade)
step Int // 0=instructions, 1=nudge, 2=urgency, 3=final
channel String @default("email") // email, sms, whatsapp
scheduledAt DateTime
sentAt DateTime?
status String @default("pending") // pending, sent, skipped, failed
payload Json?
createdAt DateTime @default(now())
@@index([pledgeId])
@@index([scheduledAt, status])
}
model Import {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
kind String // bank_statement, gocardless_export, crm_export
fileName String?
rowCount Int @default(0)
matchedCount Int @default(0)
unmatchedCount Int @default(0)
mappingConfig Json?
stats Json?
status String @default("pending") // pending, processing, completed, failed
uploadedAt DateTime @default(now())
payments Payment[]
@@index([organizationId])
}
model AnalyticsEvent {
id String @id @default(cuid())
eventType String // pledge_start, amount_selected, rail_selected, identity_submitted, pledge_completed, instruction_copy_clicked, i_paid_clicked, payment_matched
pledgeId String?
eventId String?
qrSourceId String?
metadata Json?
createdAt DateTime @default(now())
@@index([eventType])
@@index([pledgeId])
@@index([eventId])
@@index([createdAt])
}