@@ -8,6 +8,7 @@ use Modules\Booking\Enums\BookingStatus;
|
||||
use Modules\Booking\Models\Booking;
|
||||
use Modules\Payment\Data\PaymentRequestData;
|
||||
use Modules\Payment\Enums\PaymentMethod;
|
||||
use Modules\Payment\Enums\PaymentStatus;
|
||||
use Modules\Payment\Exceptions\PaymentInitiationNotAllowedException;
|
||||
use Modules\Payment\Models\Payment;
|
||||
use Modules\Payment\Services\PaymentService;
|
||||
@@ -19,6 +20,13 @@ use Modules\Payment\Services\PaymentService;
|
||||
*
|
||||
* Booking status only ever flips to `confirmed` once the gateway confirms
|
||||
* success via the webhook/verify path (T5.9/T5.10) — never here.
|
||||
*
|
||||
* Idempotent per booking: KBZ's precreate rejects a second call tied to an
|
||||
* order that's still in flight, so a repeat call (double-tap on "Pay", the
|
||||
* customer re-opening the payment screen) must not blindly precreate again.
|
||||
* If the latest attempt is still `pending`, it's re-verified against the
|
||||
* gateway (via ConfirmPaymentAction, the same logic the webhook path uses)
|
||||
* and reused instead of starting a new one.
|
||||
*/
|
||||
class InitiatePaymentAction
|
||||
{
|
||||
@@ -26,6 +34,7 @@ class InitiatePaymentAction
|
||||
|
||||
public function __construct(
|
||||
private PaymentService $paymentService,
|
||||
private ConfirmPaymentAction $confirmPayment,
|
||||
) {}
|
||||
|
||||
public function handle(Booking $booking, PaymentMethod $method = PaymentMethod::KbzMiniApp): Payment
|
||||
@@ -34,27 +43,46 @@ class InitiatePaymentAction
|
||||
throw PaymentInitiationNotAllowedException::notPendingPayment($booking);
|
||||
}
|
||||
|
||||
$merchantOrderId = $this->merchantOrderId($booking);
|
||||
return DB::transaction(function () use ($booking, $method) {
|
||||
$booking = Booking::whereKey($booking->id)->lockForUpdate()->first();
|
||||
|
||||
$result = $this->paymentService->initiate(new PaymentRequestData(
|
||||
bookingId: $booking->id,
|
||||
merchantOrderId: $merchantOrderId,
|
||||
amount: (string) $booking->price,
|
||||
currency: self::CURRENCY,
|
||||
method: $method,
|
||||
notifyUrl: $this->notifyUrl($booking, $method),
|
||||
));
|
||||
$latest = $booking->payments()->latest('id')->first();
|
||||
|
||||
return DB::transaction(fn () => Payment::create([
|
||||
'booking_id' => $booking->id,
|
||||
'gateway' => $method,
|
||||
'status' => $result->status,
|
||||
'amount' => $booking->price,
|
||||
'currency' => self::CURRENCY,
|
||||
'gateway_transaction_id' => $result->gatewayTransactionId ?? $merchantOrderId,
|
||||
'gateway_payload' => $result->gatewayPayload,
|
||||
'initiated_at' => now(),
|
||||
]));
|
||||
if ($latest !== null) {
|
||||
// No-op for an already-terminal payment (ConfirmPaymentAction
|
||||
// only re-verifies `pending` ones), so this is cheap even for
|
||||
// a Failed/Completed latest attempt — and it guards against a
|
||||
// narrow race where a webhook already completed the payment
|
||||
// but the queued booking-status listener hasn't run yet.
|
||||
$reverified = $this->confirmPayment->handle($latest->gateway, $latest->gateway_transaction_id);
|
||||
|
||||
if ($reverified !== null && $reverified->status !== PaymentStatus::Failed) {
|
||||
return $reverified;
|
||||
}
|
||||
}
|
||||
|
||||
$merchantOrderId = $this->merchantOrderId($booking);
|
||||
|
||||
$result = $this->paymentService->initiate(new PaymentRequestData(
|
||||
bookingId: $booking->id,
|
||||
merchantOrderId: $merchantOrderId,
|
||||
amount: (string) $booking->price,
|
||||
currency: self::CURRENCY,
|
||||
method: $method,
|
||||
notifyUrl: $this->notifyUrl($booking, $method),
|
||||
));
|
||||
|
||||
return Payment::create([
|
||||
'booking_id' => $booking->id,
|
||||
'gateway' => $method,
|
||||
'status' => $result->status,
|
||||
'amount' => $booking->price,
|
||||
'currency' => self::CURRENCY,
|
||||
'gateway_transaction_id' => $result->gatewayTransactionId ?? $merchantOrderId,
|
||||
'gateway_payload' => $result->gatewayPayload,
|
||||
'initiated_at' => now(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user