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, ])); } }