record; $status = $d->isConfirmed() ? '✓ Confirmed' : '✗ Incomplete'; $amount = '£' . number_format($d->amount / 100, 2); $provider = PaymentProviders::translate($d->provider_type); $badges = $status; $badges .= ' ' . $provider . ''; if ($d->isGiftAid()) { $badges .= ' Gift Aid'; } if ($d->isZakat()) { $badges .= ' Zakat'; } if ($d->reoccurrence !== -1) { $badges .= ' Recurring'; } return new HtmlString("{$amount} — " . e($d->customer?->name ?? 'Unknown donor') . '
' . $badges . '
'); } public function getSubheading(): ?string { $d = $this->record; $parts = []; $parts[] = $d->donationType?->display_name ?? 'Unknown cause'; $parts[] = $d->created_at?->format('d M Y H:i') . ' (' . $d->created_at?->diffForHumans() . ')'; if ($d->appeal) { $parts[] = 'Fundraiser: ' . $d->appeal->name; } if ($d->reference_code) { $parts[] = 'Ref: ' . $d->reference_code; } return implode(' · ', $parts); } // ── Header Actions ────────────────────────────────────────── // // Priority order: // 1. Refund (Stripe PI) — the #1 support request // 2. Cancel Recurring (Stripe SetupIntent monthly) // 3. Confirm / Unconfirm // 4. Resend Receipt // 5. Stripe Status check // 6. Open in Stripe / View Donor / Email // protected function getHeaderActions(): array { $donation = $this->record; $isStripe = $donation->provider_type === PaymentProviders::STRIPE; $isPayPal = $donation->provider_type === PaymentProviders::PAYPAL; $isGoCardless = $donation->provider_type === PaymentProviders::GOCARDLESS; $ref = $donation->provider_reference ?? ''; $hasPI = $isStripe && str_starts_with($ref, 'pi_'); $hasSI = $isStripe && str_starts_with($ref, 'seti_'); return array_values(array_filter([ // ── REFUND (Stripe PaymentIntent) ─────────────────── $hasPI && $donation->isConfirmed() ? Action::make('refund') ->label('Refund') ->icon('heroicon-o-arrow-uturn-left') ->color('danger') ->requiresConfirmation() ->modalIcon('heroicon-o-arrow-uturn-left') ->modalHeading('Refund Donation') ->modalDescription( 'Refund £' . number_format($donation->amount / 100, 2) . ' to ' . e($donation->customer?->name ?? 'donor') . '\'s card via Stripe. This cannot be undone.' ) ->modalSubmitActionLabel('Process Refund') ->form([ TextInput::make('refund_amount') ->label('Refund amount (£)') ->numeric() ->default(number_format($donation->amount / 100, 2, '.', '')) ->required() ->minValue(0.01) ->maxValue($donation->amount / 100) ->step(0.01) ->helperText('Full amount for complete refund. Reduce for partial.'), ]) ->action(function (array $data) { $donation = $this->record; $amountPence = (int) round($data['refund_amount'] * 100); $isPartial = $amountPence < $donation->amount; $service = app(StripeRefundService::class); $result = $service->refundPaymentIntent( $donation->provider_reference, $isPartial ? $amountPence : null, 'Admin refund by ' . auth()->user()?->name ); if ($result['success']) { if (! $isPartial) { $donation->donationConfirmation?->update(['confirmed_at' => null]); } $donation->internalNotes()->create([ 'user_id' => auth()->id(), 'body' => ($isPartial ? 'Partial' : 'Full') . ' refund of £' . number_format($amountPence / 100, 2) . ' processed via Stripe. Refund ID: ' . $result['refund_id'], ]); Notification::make() ->title('Refund processed') ->body('£' . number_format($result['amount'] / 100, 2) . ' refunded. ID: ' . $result['refund_id']) ->success() ->send(); } else { Notification::make() ->title('Refund failed') ->body($result['error']) ->danger() ->send(); } }) : null, // ── CANCEL RECURRING (SetupIntent monthly) ────────── $hasSI && $donation->isConfirmed() ? Action::make('cancel_recurring') ->label('Cancel Recurring') ->icon('heroicon-o-x-circle') ->color('danger') ->requiresConfirmation() ->modalIcon('heroicon-o-x-circle') ->modalHeading('Cancel Monthly Giving') ->modalDescription( 'This will detach the payment method from Stripe, preventing ' . 'all future charges for ' . e($donation->customer?->name ?? 'this donor') . '. Previously collected payments will NOT be refunded.' ) ->modalSubmitActionLabel('Cancel Monthly Giving') ->action(function () { $donation = $this->record; $service = app(StripeRefundService::class); $result = $service->detachPaymentMethod($donation->provider_reference); if ($result['success']) { $donation->donationConfirmation?->update(['confirmed_at' => null]); $donation->internalNotes()->create([ 'user_id' => auth()->id(), 'body' => 'Monthly giving cancelled. ' . ($result['message'] ?? 'Payment method detached from Stripe.'), ]); Notification::make() ->title('Monthly giving cancelled') ->body($result['message'] ?? 'Payment method detached') ->success() ->send(); } else { Notification::make() ->title('Cancellation failed') ->body($result['error']) ->danger() ->send(); } }) : null, // ── OPEN PAYPAL ───────────────────────────────────── $isPayPal && $ref ? Action::make('open_paypal') ->label('Open PayPal') ->icon('heroicon-o-arrow-top-right-on-square') ->color('warning') ->url('https://www.paypal.com/activity/payment/' . urlencode($ref)) ->openUrlInNewTab() : null, // ── OPEN GOCARDLESS ───────────────────────────────── $isGoCardless && $ref ? Action::make('open_gocardless') ->label('Open GoCardless') ->icon('heroicon-o-arrow-top-right-on-square') ->color('warning') ->url('https://manage.gocardless.com/payments/' . urlencode($ref)) ->openUrlInNewTab() : null, // ── UNCONFIRM ─────────────────────────────────────── $donation->isConfirmed() ? Action::make('unconfirm') ->label('Unconfirm') ->icon('heroicon-o-x-mark') ->color('warning') ->requiresConfirmation() ->modalHeading('Unconfirm Donation') ->modalDescription( 'Mark this donation as incomplete. This does NOT refund money ' . 'on Stripe/PayPal — use the Refund button for that.' ) ->action(function () { $donation = $this->record; $donation->donationConfirmation?->update(['confirmed_at' => null]); $donation->internalNotes()->create([ 'user_id' => auth()->id(), 'body' => 'Donation manually unconfirmed by admin.', ]); Notification::make()->title('Donation unconfirmed')->warning()->send(); }) : null, // ── CONFIRM ───────────────────────────────────────── ! $donation->isConfirmed() ? Action::make('confirm') ->label('Confirm') ->icon('heroicon-o-check-circle') ->color('success') ->requiresConfirmation() ->modalHeading('Confirm Donation') ->modalDescription( 'Mark this donation as confirmed. Only do this if you have ' . 'verified the payment was received.' ) ->action(function () { $donation = $this->record; $donation->confirm(); $donation->internalNotes()->create([ 'user_id' => auth()->id(), 'body' => 'Donation manually confirmed by admin.', ]); Notification::make()->title('Donation confirmed')->success()->send(); }) : null, // ── RESEND RECEIPT ────────────────────────────────── $donation->isConfirmed() && $donation->customer?->email ? Action::make('resend_receipt') ->label('Receipt') ->icon('heroicon-o-envelope') ->color('gray') ->requiresConfirmation() ->modalDescription('Send receipt to ' . $donation->customer->email) ->action(function () { $donation = $this->record; try { Mail::to($donation->customer->email) ->send(new DonationConfirmed($donation)); Notification::make() ->title('Receipt sent to ' . $donation->customer->email) ->success() ->send(); } catch (\Throwable $e) { Log::error($e); Notification::make() ->title('Failed to send receipt') ->body($e->getMessage()) ->danger() ->send(); } }) : null, // ── STRIPE STATUS ─────────────────────────────────── ($hasPI || $hasSI) ? Action::make('check_stripe') ->label('Stripe Status') ->icon('heroicon-o-magnifying-glass') ->color('gray') ->action(function () use ($hasPI) { $donation = $this->record; $service = app(StripeRefundService::class); $details = $hasPI ? $service->getPaymentDetails($donation->provider_reference) : $service->getSetupIntentDetails($donation->provider_reference); if (! $details) { Notification::make() ->title('Could not retrieve Stripe details') ->danger() ->send(); return; } $lines = []; foreach ($details as $key => $val) { if ($val === null || $val === '') { continue; } $label = str_replace('_', ' ', ucfirst($key)); if (is_bool($val)) { $val = $val ? 'Yes' : 'No'; } if ($key === 'amount' || $key === 'amount_refunded') { $val = '£' . number_format($val / 100, 2); } $lines[] = "{$label}: {$val}"; } Notification::make() ->title('Stripe Details') ->body(implode("\n", $lines)) ->info() ->persistent() ->send(); }) : null, // ── OPEN IN STRIPE ────────────────────────────────── $isStripe && $ref ? Action::make('open_stripe') ->label('Stripe ↗') ->icon('heroicon-o-arrow-top-right-on-square') ->color('gray') ->url('https://dashboard.stripe.com/search?query=' . urlencode($ref)) ->openUrlInNewTab() : null, // ── VIEW DONOR ────────────────────────────────────── $donation->customer_id ? Action::make('view_donor') ->label('Donor') ->icon('heroicon-o-user') ->color('gray') ->url(CustomerResource::getUrl('edit', ['record' => $donation->customer_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, ])); } // Hide save/cancel — donations are not directly editable protected function getSaveFormAction(): Action { return parent::getSaveFormAction()->visible(false); } protected function getCancelFormAction(): Action { return parent::getCancelFormAction()->visible(false); } }