CORE PRODUCT SHIFT: A pledge is now a promise to pay on a future date, not just 'pay now'. NEW FLOW: Amount → Schedule → Payment/Identity → Confirmation SCHEDULE STEP (/p/[token] step 1): - 'Pay right now' — existing card/DD/bank flow - 'Pay on a specific date' — calendar picker with smart suggestions (This Friday, End of month, Payday 1st, In 2 weeks, In 1 month) - 'Split into monthly payments' — 2/3/4/6/12 month installment plans with per-installment breakdown and date schedule SCHEMA CHANGES: - Pledge.dueDate — when the donor promises to pay (null = now) - Pledge.planId — groups installment pledges together - Pledge.installmentNumber / installmentTotal — e.g. 2 of 4 - Pledge.reminderSentForDueDate — tracking flag - New indexes on dueDate+status and planId INSTALLMENT PLANS: - Creates N linked Pledge records with shared planId - Each installment gets its own reference, due date, reminders - Reminders: 2 days before, on due date, 3 days after, 10 days after - WhatsApp receipt shows full plan summary DEFERRED SINGLE PLEDGES: - Reminders anchored to due date, not creation date - 'Pay on date' → reminders: 2 days before, on day, +3d nudge, +10d final - WhatsApp preferred when phone number provided DASHBOARD: - API returns dueDate, planId, installment info for each pledge - Confirmation step shows schedule details for deferred pledges
218 lines
6.6 KiB
Plaintext
218 lines
6.6 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
|
|
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")
|
|
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
|
|
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])
|
|
}
|