Complete Payment module: initiate/webhook/confirm/refund actions, Filament resources (T5.8-T5.13)
- InitiatePaymentAction + POST /api/v1/payments/{booking}/initiate
- Generic KBZ webhook (POST /api/v1/webhooks/{method}/{encryptBookingId?}),
signature verification per KBZ's real callback spec, PaymentGatewayInterface::handleWebhook()
- ConfirmPaymentAction: idempotent confirmation, PaymentCompleted/PaymentFailed events,
MarkBookingPaid listener
- RefundBookingAction + POST /api/v1/bookings/{booking}/refund: partial refunds validated
against remaining balance, RefundProcessed event, MarkBookingRefunded listener
- CancelBookingAction now refunds confirmed bookings instead of rejecting; BookingPolicy::cancel
requires process_refunds for confirmed bookings
- PaymentPlugin + PaymentResource/RefundResource Filament admin UI (read-only payments,
refund list + Process action)
- Booking detail page now shows related payments
- Fix CACHE_STORE mismatch (database -> redis) so tagged route caching works
- CLAUDE.md: never run migrate:fresh/migrate:refresh/db:wipe on dev without being asked
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Payment\Filament\Resources\Refunds\Actions;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Modules\Payment\Actions\RefundBookingAction;
|
||||
use Modules\Payment\Enums\PaymentStatus;
|
||||
use Modules\Payment\Exceptions\RefundFailedException;
|
||||
use Modules\Payment\Exceptions\RefundNotAllowedException;
|
||||
use Modules\Payment\Models\Payment;
|
||||
|
||||
/**
|
||||
* Header action on ListRefunds — staff pick a successful Payment (only
|
||||
* Completed ones are offered, domain.md §6) and an amount/reason, which
|
||||
* calls RefundBookingAction the same way the API endpoint does (T5.11).
|
||||
*/
|
||||
class ProcessRefundAction
|
||||
{
|
||||
public static function make(): Action
|
||||
{
|
||||
return Action::make('process')
|
||||
->label('Process Refund')
|
||||
->icon(Heroicon::OutlinedReceiptRefund)
|
||||
->color('danger')
|
||||
->visible(fn (): bool => auth()->user()?->can('process_refunds') ?? false)
|
||||
->schema([
|
||||
Select::make('payment_id')
|
||||
->label('Payment')
|
||||
->options(fn () => Payment::query()
|
||||
->where('status', PaymentStatus::Completed->value)
|
||||
->with('booking')
|
||||
->get()
|
||||
->mapWithKeys(fn (Payment $payment) => [
|
||||
$payment->id => "{$payment->booking?->booking_ref} — {$payment->amount} {$payment->currency} (#{$payment->id})",
|
||||
]))
|
||||
->searchable()
|
||||
->required(),
|
||||
TextInput::make('amount')
|
||||
->numeric()
|
||||
->minValue(0.01)
|
||||
->required(),
|
||||
Textarea::make('reason')
|
||||
->required(),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
$payment = Payment::with('booking')->findOrFail($data['payment_id']);
|
||||
|
||||
try {
|
||||
app(RefundBookingAction::class)->handle(
|
||||
$payment->booking,
|
||||
(string) $data['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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user