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