231f5679ef
- New RefundBookingTableAction on the booking list row and detail page, refunding a Confirmed booking directly via RefundBookingAction — no need to hunt up its Payment on the Refunds resource first. - Payment::refundableBalance() extracted from RefundBookingAction's private balance check so both refund forms can display and cap against it. - RefundBookingAction::resolveRefundablePayment() made public for the same reason (round-trip leg resolution reused by the UI). - Both refund forms (ProcessRefundAction and the new booking action) gain a "Full refund" toggle, on by default, which refunds the payment's whole remaining balance without requiring a manually typed amount. Turning it off reveals an amount field capped at the refundable balance.
78 lines
3.2 KiB
PHP
78 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace Modules\Payment\Filament\Actions;
|
|
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Support\Icons\Heroicon;
|
|
use Modules\Booking\Enums\BookingStatus;
|
|
use Modules\Booking\Models\Booking;
|
|
use Modules\Payment\Actions\RefundBookingAction;
|
|
use Modules\Payment\Exceptions\RefundFailedException;
|
|
use Modules\Payment\Exceptions\RefundNotAllowedException;
|
|
|
|
/**
|
|
* Shared between BookingsTable (row action) and ViewBooking (header action)
|
|
* in the Booking module — lets staff refund a booking directly instead of
|
|
* hunting up its Payment on the Refunds resource (ProcessRefundAction). Both
|
|
* surfaces call the same RefundBookingAction used by the API.
|
|
*/
|
|
class RefundBookingTableAction
|
|
{
|
|
public static function make(): Action
|
|
{
|
|
return Action::make('refund')
|
|
->label('Refund')
|
|
->icon(Heroicon::OutlinedReceiptRefund)
|
|
->color('danger')
|
|
->visible(fn (): bool => auth()->user()?->can('process_refunds') ?? false)
|
|
->disabled(fn (Booking $record): bool => $record->status !== BookingStatus::Confirmed)
|
|
->schema([
|
|
Toggle::make('full_refund')
|
|
->label('Full refund')
|
|
->live()
|
|
->default(true)
|
|
->helperText(fn (Booking $record): string => 'Refundable balance: '.(app(RefundBookingAction::class)
|
|
->resolveRefundablePayment($record)?->refundableBalance() ?? '0.00')),
|
|
TextInput::make('amount')
|
|
->numeric()
|
|
->minValue(0.01)
|
|
->visible(fn (Get $get): bool => ! $get('full_refund'))
|
|
->required(fn (Get $get): bool => ! $get('full_refund'))
|
|
->maxValue(fn (Booking $record): ?string => app(RefundBookingAction::class)
|
|
->resolveRefundablePayment($record)?->refundableBalance()),
|
|
Textarea::make('reason')
|
|
->required(),
|
|
])
|
|
->action(function (Booking $record, array $data): void {
|
|
$amount = $data['full_refund']
|
|
? app(RefundBookingAction::class)->resolveRefundablePayment($record)?->refundableBalance() ?? '0.00'
|
|
: (string) $data['amount'];
|
|
|
|
try {
|
|
app(RefundBookingAction::class)->handle(
|
|
$record,
|
|
$amount,
|
|
$data['reason'],
|
|
auth()->id(),
|
|
);
|
|
|
|
Notification::make()
|
|
->title('Refund processed')
|
|
->success()
|
|
->send();
|
|
} catch (RefundNotAllowedException|RefundFailedException $exception) {
|
|
Notification::make()
|
|
->title('Refund failed')
|
|
->body($exception->getMessage())
|
|
->danger()
|
|
->send();
|
|
}
|
|
});
|
|
}
|
|
}
|