Add refund action to booking list/detail with full-refund toggle
- 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.
This commit is contained in:
@@ -35,14 +35,7 @@ class RefundBookingAction
|
||||
throw RefundNotAllowedException::notConfirmed($booking);
|
||||
}
|
||||
|
||||
// Round trip: payment is combined on the outbound leg, so a return
|
||||
// leg has no Payment of its own — refund against its linked leg's
|
||||
// Payment instead (domain.md §2b). The Confirmed check above still
|
||||
// applies to $booking itself, not the payment holder, so each leg
|
||||
// remains independently cancellable/refundable.
|
||||
$paymentBooking = $booking->is_return_leg ? ($booking->linkedBooking ?? $booking) : $booking;
|
||||
|
||||
$payment = $paymentBooking->payments()->where('status', PaymentStatus::Completed->value)->latest()->first();
|
||||
$payment = $this->resolveRefundablePayment($booking);
|
||||
|
||||
if ($payment === null) {
|
||||
throw RefundNotAllowedException::noCompletedPayment($booking);
|
||||
@@ -80,10 +73,27 @@ class RefundBookingAction
|
||||
return $refund;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Completed Payment a refund against $booking would apply to.
|
||||
* Public so the Filament refund forms can look up the same Payment to
|
||||
* surface its refundable balance before staff submit an amount.
|
||||
*
|
||||
* Round trip: payment is combined on the outbound leg, so a return leg
|
||||
* has no Payment of its own — resolve against its linked leg's Payment
|
||||
* instead (domain.md §2b). The Confirmed check in handle() still applies
|
||||
* to $booking itself, not the payment holder, so each leg remains
|
||||
* independently cancellable/refundable.
|
||||
*/
|
||||
public function resolveRefundablePayment(Booking $booking): ?Payment
|
||||
{
|
||||
$paymentBooking = $booking->is_return_leg ? ($booking->linkedBooking ?? $booking) : $booking;
|
||||
|
||||
return $paymentBooking->payments()->where('status', PaymentStatus::Completed->value)->latest()->first();
|
||||
}
|
||||
|
||||
private function assertWithinRefundableBalance(Payment $payment, string $amount): void
|
||||
{
|
||||
$alreadyRefunded = (string) $payment->refunds()->where('status', RefundStatus::Completed->value)->sum('amount');
|
||||
$remaining = bcsub((string) $payment->amount, $alreadyRefunded, 2);
|
||||
$remaining = $payment->refundableBalance();
|
||||
|
||||
if (bccomp($amount, $remaining, 2) === 1) {
|
||||
throw RefundNotAllowedException::exceedsRefundableBalance($payment, $amount, $remaining);
|
||||
|
||||
Reference in New Issue
Block a user