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:
@@ -20,88 +20,85 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
$current = $this->currentSeasonScope()->count();
|
||||
$current = $this->currentSeasonCount();
|
||||
return "{$current} subscribers this season.";
|
||||
}
|
||||
|
||||
/** Real subscriber: has customer, has payments, amount > 0, not soft-deleted */
|
||||
private function realScope(): Builder
|
||||
/** Count real current-season subscribers using the model directly (safe) */
|
||||
private function currentSeasonCount(): int
|
||||
{
|
||||
return ScheduledGivingDonation::query()
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('scheduled_giving_donations.deleted_at')
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'));
|
||||
->whereNull('deleted_at')
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
|
||||
->count();
|
||||
}
|
||||
|
||||
/** Current season: real + has at least one future payment */
|
||||
private function currentSeasonScope(): Builder
|
||||
{
|
||||
return $this->realScope()
|
||||
->whereHas('payments', fn ($q) => $q
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
/** Applies real + current season filters to the query */
|
||||
private function applyCurrentSeason(Builder $q): Builder
|
||||
{
|
||||
return $q
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('scheduled_giving_donations.deleted_at')
|
||||
->whereHas('payments', fn ($sub) => $sub->whereNull('deleted_at'))
|
||||
->whereHas('payments', fn ($sub) => $sub
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
/** Applies real + expired (no future payments) filters */
|
||||
private function applyExpired(Builder $q): Builder
|
||||
{
|
||||
return $q
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('scheduled_giving_donations.deleted_at')
|
||||
->whereHas('payments', fn ($sub) => $sub->whereNull('deleted_at'))
|
||||
->whereDoesntHave('payments', fn ($sub) => $sub
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
/** Applies real subscriber filters */
|
||||
/**
|
||||
* Apply "real subscriber" filter using whereIn subqueries
|
||||
* instead of whereHas — avoids null model crash during tab init.
|
||||
*/
|
||||
private function applyReal(Builder $q): Builder
|
||||
{
|
||||
return $q
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('scheduled_giving_donations.deleted_at')
|
||||
->whereHas('payments', fn ($sub) => $sub->whereNull('deleted_at'));
|
||||
->whereIn('scheduled_giving_donations.id', fn ($sub) => $sub
|
||||
->select('scheduled_giving_donation_id')
|
||||
->from('scheduled_giving_payments')
|
||||
->whereNull('deleted_at'));
|
||||
}
|
||||
|
||||
/** Real + has future payment = current season */
|
||||
private function applyCurrentSeason(Builder $q): Builder
|
||||
{
|
||||
return $this->applyReal($q)
|
||||
->whereIn('scheduled_giving_donations.id', fn ($sub) => $sub
|
||||
->select('scheduled_giving_donation_id')
|
||||
->from('scheduled_giving_payments')
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
/** Real + NO future payments = expired */
|
||||
private function applyExpired(Builder $q): Builder
|
||||
{
|
||||
return $this->applyReal($q)
|
||||
->whereNotIn('scheduled_giving_donations.id', fn ($sub) => $sub
|
||||
->select('scheduled_giving_donation_id')
|
||||
->from('scheduled_giving_payments')
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()));
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$campaigns = ScheduledGivingCampaign::all();
|
||||
|
||||
$currentCount = $this->currentSeasonScope()->count();
|
||||
$currentCount = $this->currentSeasonCount();
|
||||
|
||||
$tabs = [];
|
||||
|
||||
// Current season — the primary tab
|
||||
$tabs['current'] = Tab::make('This Season')
|
||||
->icon('heroicon-o-sun')
|
||||
->badge($currentCount)
|
||||
->badgeColor('success')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyCurrentSeason($q));
|
||||
|
||||
// Per-campaign tabs for current season
|
||||
foreach ($campaigns as $c) {
|
||||
$slug = str($c->title)->slug()->toString();
|
||||
$count = $this->currentSeasonScope()
|
||||
$count = ScheduledGivingDonation::query()
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('deleted_at')
|
||||
->where('scheduled_giving_campaign_id', $c->id)
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
|
||||
->count();
|
||||
|
||||
if ($count === 0) continue; // Skip campaigns with no current subscribers
|
||||
if ($count === 0) continue;
|
||||
|
||||
$tabs[$slug] = Tab::make($c->title)
|
||||
->icon('heroicon-o-calendar')
|
||||
@@ -111,12 +108,14 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
->where('scheduled_giving_campaign_id', $c->id));
|
||||
}
|
||||
|
||||
// Failed (current season only)
|
||||
$failedCount = $this->currentSeasonScope()
|
||||
->whereHas('payments', fn ($q) => $q
|
||||
->where('is_paid', false)
|
||||
->where('attempts', '>', 0)
|
||||
->whereNull('deleted_at'))
|
||||
// Failed (current season)
|
||||
$failedCount = ScheduledGivingDonation::query()
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('deleted_at')
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
|
||||
->whereHas('payments', fn ($q) => $q->where('is_paid', false)->where('attempts', '>', 0)->whereNull('deleted_at'))
|
||||
->count();
|
||||
|
||||
if ($failedCount > 0) {
|
||||
@@ -125,17 +124,21 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
->badge($failedCount)
|
||||
->badgeColor('danger')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyCurrentSeason($q)
|
||||
->whereHas('payments', fn ($sub) => $sub
|
||||
->whereIn('scheduled_giving_donations.id', fn ($sub) => $sub
|
||||
->select('scheduled_giving_donation_id')
|
||||
->from('scheduled_giving_payments')
|
||||
->where('is_paid', false)
|
||||
->where('attempts', '>', 0)
|
||||
->whereNull('deleted_at')));
|
||||
}
|
||||
|
||||
// Past seasons
|
||||
$expiredCount = $this->realScope()
|
||||
->whereDoesntHave('payments', fn ($q) => $q
|
||||
->whereNull('deleted_at')
|
||||
->where('expected_at', '>', now()))
|
||||
$expiredCount = ScheduledGivingDonation::query()
|
||||
->whereNotNull('customer_id')
|
||||
->where('total_amount', '>', 0)
|
||||
->whereNull('deleted_at')
|
||||
->whereHas('payments', fn ($q) => $q->whereNull('deleted_at'))
|
||||
->whereDoesntHave('payments', fn ($q) => $q->whereNull('deleted_at')->where('expected_at', '>', now()))
|
||||
->count();
|
||||
|
||||
$tabs['past'] = Tab::make('Past Seasons')
|
||||
@@ -144,7 +147,6 @@ class ListScheduledGivingDonations extends ListRecords
|
||||
->badgeColor('gray')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyExpired($q));
|
||||
|
||||
// All real
|
||||
$tabs['all'] = Tab::make('All')
|
||||
->icon('heroicon-o-squares-2x2')
|
||||
->modifyQueryUsing(fn (Builder $q) => $this->applyReal($q));
|
||||
|
||||
Reference in New Issue
Block a user