record;
$status = $d->is_active
? '● Active'
: '● Cancelled';
$amount = '£' . number_format($d->total_amount / 100, 2);
// Payment progress
$totalPayments = $d->payments()->count();
$paidPayments = $d->payments()->where('is_paid', true)->count();
$paidTotal = $d->payments()->where('is_paid', true)->sum('amount') / 100;
$progress = $totalPayments > 0
? ''
. $paidPayments . '/' . $totalPayments . ' payments · £' . number_format($paidTotal, 2) . ' collected'
: 'No payments generated';
if ($d->is_zakat) {
$progress .= ' Zakat';
}
if ($d->is_gift_aid) {
$progress .= ' Gift Aid';
}
return new HtmlString(
$amount . '/night — ' . e($d->customer?->name ?? 'Unknown')
. '
' . $status . ' ' . $progress . '
'
);
}
public function getSubheading(): ?string
{
$d = $this->record;
$parts = [];
$parts[] = $d->scheduledGivingCampaign?->title ?? 'Unknown campaign';
$parts[] = 'Started ' . $d->created_at?->format('d M Y');
if ($d->customer?->email) {
$parts[] = $d->customer->email;
}
if ($d->reference_code) {
$parts[] = 'Ref: ' . $d->reference_code;
}
return implode(' · ', $parts);
}
// ── Header Actions ──────────────────────────────────────────
//
// Priority order:
// 1. Cancel (deactivate + detach card)
// 2. Cancel & Refund All
// 3. Activate
// 4. Stripe Status
// 5. Open in Stripe / View Donor / View Appeal / Email
//
public function getHeaderActions(): array
{
$donation = $this->record;
$hasStripe = (bool) $donation->stripe_setup_intent_id;
$paidCount = $donation->payments()
->where('is_paid', true)
->whereNotNull('stripe_payment_intent_id')
->where('stripe_payment_intent_id', '!=', 'SKIPPED')
->count();
$paidTotal = $donation->payments()
->where('is_paid', true)
->whereNotNull('stripe_payment_intent_id')
->where('stripe_payment_intent_id', '!=', 'SKIPPED')
->sum('amount') / 100;
return array_values(array_filter([
// ── CANCEL ──────────────────────────────────────────
$donation->is_active ? Action::make('cancel')
->label('Cancel')
->icon('heroicon-o-x-circle')
->color('danger')
->requiresConfirmation()
->modalIcon('heroicon-o-x-circle')
->modalHeading('Cancel Regular Giving')
->modalDescription(
'This will deactivate ' . e($donation->customer?->name ?? 'this donor') . '\'s regular giving.'
. ($hasStripe ? "\n\nThe payment method will be detached from Stripe, preventing any future charges." : '')
. "\n\nPreviously collected payments will NOT be refunded."
)
->modalSubmitActionLabel('Cancel Regular Giving')
->action(function () {
$donation = $this->record;
$hasStripe = (bool) $donation->stripe_setup_intent_id;
$donation->update(['is_active' => false]);
$stripeMsg = '';
if ($hasStripe) {
$service = app(StripeRefundService::class);
$result = $service->detachPaymentMethod($donation->stripe_setup_intent_id);
$stripeMsg = $result['success']
? ($result['message'] ?? 'Payment method detached.')
: 'Warning: ' . ($result['error'] ?? 'Could not detach payment method.');
}
$donation->internalNotes()->create([
'user_id' => auth()->id(),
'body' => 'Regular giving cancelled by admin. ' . $stripeMsg,
]);
Notification::make()
->title('Regular giving cancelled')
->body($stripeMsg ?: 'Deactivated')
->success()
->send();
}) : null,
// ── CANCEL & REFUND ALL ─────────────────────────────
$donation->is_active && $paidCount > 0 ? Action::make('cancel_and_refund')
->label('Cancel & Refund All')
->icon('heroicon-o-arrow-uturn-left')
->color('danger')
->requiresConfirmation()
->modalIcon('heroicon-o-exclamation-triangle')
->modalHeading('Cancel & Refund All Payments')
->modalDescription(
"This will:\n"
. "1. Deactivate the regular giving\n"
. "2. Detach the payment method from Stripe\n"
. "3. Refund all {$paidCount} collected payments (£" . number_format($paidTotal, 2) . ")\n\n"
. "This cannot be undone."
)
->modalSubmitActionLabel('Cancel & Refund Everything')
->action(function () {
$donation = $this->record;
$donation->update(['is_active' => false]);
$service = app(StripeRefundService::class);
// Detach card
if ($donation->stripe_setup_intent_id) {
$service->detachPaymentMethod($donation->stripe_setup_intent_id);
}
// Refund all paid payments
$paidPayments = $donation->payments()
->where('is_paid', true)
->whereNotNull('stripe_payment_intent_id')
->where('stripe_payment_intent_id', '!=', 'SKIPPED')
->get();
$refunded = 0;
$failed = 0;
$totalRefunded = 0;
foreach ($paidPayments as $payment) {
$result = $service->refundPaymentIntent(
$payment->stripe_payment_intent_id,
null,
'Bulk cancel & refund — payment #' . $payment->id
);
if ($result['success']) {
$payment->update(['is_paid' => false]);
$refunded++;
$totalRefunded += $result['amount'];
} else {
$failed++;
}
}
$donation->internalNotes()->create([
'user_id' => auth()->id(),
'body' => 'Cancelled & refunded all payments. '
. $refunded . ' refunded (£' . number_format($totalRefunded / 100, 2) . ')'
. ($failed > 0 ? ", {$failed} failed" : ''),
]);
Notification::make()
->title('Cancelled & Refunded')
->body(
$refunded . ' payments refunded (£' . number_format($totalRefunded / 100, 2) . ')'
. ($failed > 0 ? ". {$failed} failed — check Stripe." : '')
)
->success()
->send();
}) : null,
// ── REACTIVATE ──────────────────────────────────────
! $donation->is_active ? Action::make('activate')
->label('Reactivate')
->icon('heroicon-o-play')
->color('success')
->requiresConfirmation()
->modalHeading('Reactivate Regular Giving')
->modalDescription(
'Re-enable payment collection. Note: if the payment method '
. 'was previously detached, future charges may fail.'
)
->action(function () {
$donation = $this->record;
$donation->update(['is_active' => true]);
$donation->internalNotes()->create([
'user_id' => auth()->id(),
'body' => 'Regular giving reactivated by admin.',
]);
Notification::make()->title('Regular giving reactivated')->success()->send();
}) : null,
// ── STRIPE STATUS ───────────────────────────────────
$hasStripe ? Action::make('stripe_status')
->label('Stripe Status')
->icon('heroicon-o-magnifying-glass')
->color('gray')
->action(function () {
$donation = $this->record;
$service = app(StripeRefundService::class);
$details = $service->getSetupIntentDetails($donation->stripe_setup_intent_id);
if (! $details) {
Notification::make()
->title('Could not retrieve Stripe details')
->danger()
->send();
return;
}
$body = 'Status: ' . $details['status'];
if ($details['card_brand'] && $details['card_last4']) {
$body .= "\nCard: " . ucfirst($details['card_brand']) . ' ····' . $details['card_last4'];
}
if ($details['card_exp_month'] && $details['card_exp_year']) {
$body .= ' (expires ' . $details['card_exp_month'] . '/' . $details['card_exp_year'] . ')';
}
$body .= "\nCreated: " . $details['created'];
if ($details['customer_id']) {
$body .= "\nStripe Customer: " . $details['customer_id'];
}
Notification::make()
->title('Stripe SetupIntent')
->body($body)
->info()
->persistent()
->send();
}) : null,
// ── OPEN IN STRIPE ──────────────────────────────────
$hasStripe ? Action::make('open_stripe')
->label('Stripe ↗')
->icon('heroicon-o-arrow-top-right-on-square')
->color('gray')
->url('https://dashboard.stripe.com/search?query=' . urlencode($donation->stripe_setup_intent_id))
->openUrlInNewTab() : null,
// ── VIEW DONOR ──────────────────────────────────────
$donation->customer ? Action::make('view_customer')
->label('Donor')
->icon('heroicon-o-user')
->color('gray')
->url(EditCustomer::getUrl(['record' => $donation->customer->id]))
: null,
// ── VIEW APPEAL ─────────────────────────────────────
$donation->appeal ? Action::make('view_appeal')
->label('Fundraiser')
->icon('heroicon-o-heart')
->color('gray')
->url(EditAppeal::getUrl(['record' => $donation->appeal->id]))
: null,
// ── EMAIL ───────────────────────────────────────────
$donation->customer?->email ? Action::make('email')
->label('Email')
->icon('heroicon-o-at-symbol')
->color('gray')
->url('mailto:' . $donation->customer->email)
->openUrlInNewTab() : null,
]));
}
}