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
54 lines
1.9 KiB
TypeScript
54 lines
1.9 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
export const createEventSchema = z.object({
|
|
name: z.string().min(1).max(200),
|
|
description: z.string().max(2000).optional(),
|
|
eventDate: z.string().datetime().optional(),
|
|
location: z.string().max(500).optional(),
|
|
goalAmount: z.number().int().positive().optional(), // pence
|
|
currency: z.string().default('GBP'),
|
|
})
|
|
|
|
export const createQrSourceSchema = z.object({
|
|
label: z.string().min(1).max(100),
|
|
volunteerName: z.string().max(100).optional(),
|
|
tableName: z.string().max(100).optional(),
|
|
})
|
|
|
|
export const createPledgeSchema = z.object({
|
|
amountPence: z.number().int().min(100).max(100000000), // £1 to £1M
|
|
rail: z.enum(['bank', 'gocardless', 'card']),
|
|
donorName: z.string().max(200).optional().default(''),
|
|
donorEmail: z.string().max(200).optional().default(''),
|
|
donorPhone: z.string().max(20).optional().default(''),
|
|
giftAid: z.boolean().default(false),
|
|
eventId: z.string(),
|
|
qrSourceId: z.string().nullable().optional(),
|
|
// Payment scheduling
|
|
scheduleMode: z.enum(['now', 'date', 'installments']).default('now'),
|
|
dueDate: z.string().optional(),
|
|
installmentCount: z.number().int().min(2).max(12).optional(),
|
|
installmentDates: z.array(z.string()).optional(),
|
|
}).transform((data) => ({
|
|
...data,
|
|
donorEmail: data.donorEmail && data.donorEmail.includes('@') ? data.donorEmail : undefined,
|
|
donorPhone: data.donorPhone && data.donorPhone.length >= 10 ? data.donorPhone : undefined,
|
|
donorName: data.donorName || undefined,
|
|
qrSourceId: data.qrSourceId || undefined,
|
|
}))
|
|
|
|
export const importBankStatementSchema = z.object({
|
|
columnMapping: z.object({
|
|
dateCol: z.string(),
|
|
descriptionCol: z.string(),
|
|
amountCol: z.string().optional(),
|
|
creditCol: z.string().optional(),
|
|
referenceCol: z.string().optional(),
|
|
}),
|
|
})
|
|
|
|
export const updatePledgeStatusSchema = z.object({
|
|
status: z.enum(['new', 'initiated', 'paid', 'overdue', 'cancelled']),
|
|
notes: z.string().max(1000).optional(),
|
|
})
|