feat: conditional & match funding pledges — deeply integrated across entire product

- Schema: isConditional, conditionType, conditionText, conditionThreshold, conditionMet, conditionMetAt on Pledge
- Pledge form: 'This is a match pledge' toggle after amount selection
  - Two modes: threshold (if target is reached) and match (match funding)
  - Goal amount passed through from event
- Auto-trigger: when total raised hits threshold, conditional pledges unlock automatically
  - WhatsApp notification sent to donor when unlocked
  - Threshold check runs after every pledge creation AND every status change
- Cron: skips conditional pledges until conditionMet=true (no premature reminders)
- Dashboard Home: progress bar shows conditional segment (amber), stats grid adds Conditional column
- Dashboard Money: conditional/unlocked badge on pledge rows
- Dashboard Collect: hero shows conditional total in amber
- Dashboard Reports: financial summary shows conditional breakdown
- Donor 'My Pledges': conditional card with condition text + activation status
- Confirmation step: specialized messaging for match pledges
- CRM export: includes is_conditional, condition_type, condition_text, condition_met columns
- Status guide: conditional status explained in human language
This commit is contained in:
2026-03-05 04:19:23 +08:00
parent c11bf4bea7
commit 50d449e2b7
23 changed files with 607 additions and 140 deletions

View File

@@ -33,6 +33,11 @@ export interface PledgeData {
dueDate?: string
installmentCount?: number
installmentDates?: string[]
// Conditional / match funding
isConditional: boolean
conditionType?: "threshold" | "match" | "custom"
conditionText?: string
conditionThreshold?: number
}
interface EventInfo {
@@ -46,6 +51,7 @@ interface EventInfo {
externalPlatform: string | null
zakatEligible: boolean
hasStripe: boolean
goalAmount: number | null
}
/*
@@ -78,6 +84,7 @@ export default function PledgePage() {
emailOptIn: false,
whatsappOptIn: false,
scheduleMode: "now",
isConditional: false,
})
const [pledgeResult, setPledgeResult] = useState<{
id: string
@@ -106,14 +113,14 @@ export default function PledgePage() {
const isExternal = eventInfo?.paymentMode === "external" && eventInfo?.externalUrl
// Step 0: Amount selected
const handleAmountSelected = (amountPence: number) => {
setPledgeData((d) => ({ ...d, amountPence }))
const handleAmountSelected = (amountPence: number, conditional?: { isConditional: boolean; conditionType?: "threshold" | "match" | "custom"; conditionText?: string; conditionThreshold?: number }) => {
const conditionalData = conditional || { isConditional: false }
setPledgeData((d) => ({ ...d, amountPence, ...conditionalData }))
if (isExternal) {
// External events: amount → identity → redirect (skip schedule + payment method)
setPledgeData((d) => ({ ...d, amountPence, rail: "bank", scheduleMode: "now" }))
setStep(3) // → Identity
setPledgeData((d) => ({ ...d, amountPence, rail: "bank", scheduleMode: "now", ...conditionalData }))
setStep(3)
} else {
setStep(1) // → Schedule step
setStep(1)
}
}
@@ -225,7 +232,7 @@ export default function PledgePage() {
: undefined
const steps: Record<number, React.ReactNode> = {
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} />,
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} eventId={eventInfo?.id} goalAmount={eventInfo?.goalAmount} />,
1: <ScheduleStep amount={pledgeData.amountPence} onSelect={handleScheduleSelected} />,
2: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} hasStripe={eventInfo?.hasStripe ?? false} />,
3: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} zakatEligible={eventInfo?.zakatEligible} orgName={eventInfo?.organizationName} />,
@@ -242,6 +249,8 @@ export default function PledgePage() {
dueDateLabel={dueDateLabel}
installmentCount={pledgeData.installmentCount}
installmentAmount={pledgeData.installmentCount ? Math.ceil(pledgeData.amountPence / pledgeData.installmentCount) : undefined}
isConditional={pledgeData.isConditional}
conditionText={pledgeData.conditionText}
/>
),
6: <CardPaymentStep amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} eventId={eventInfo?.id || ""} qrSourceId={eventInfo?.qrSourceId || null} onComplete={submitPledge} />,