| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- <?php
- namespace Webkul\BagistoApi\Jobs;
- use Illuminate\Bus\Queueable;
- use Illuminate\Contracts\Queue\ShouldQueue;
- use Illuminate\Foundation\Bus\Dispatchable;
- use Illuminate\Queue\InteractsWithQueue;
- use Illuminate\Queue\SerializesModels;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Event;
- use Illuminate\Support\Facades\Log;
- use Webkul\Paypal\Payment\SmartButton;
- use Webkul\Sales\Models\Order;
- use Webkul\Sales\Models\OrderProxy;
- use Webkul\Sales\Repositories\OrderRepository;
- /**
- * Delayed reconciliation for orders that are still PENDING / PENDING_PAYMENT
- * after the configured grace period.
- *
- * Behaviour:
- *
- * - If the order has already moved past pending we exit cleanly.
- * - If we can look up the gateway order id and it has been captured,
- * we DO NOT auto-mark the Bagisto order as paid; instead we emit
- * `bagistoapi.payment.reconcile.captured` so an ops listener can
- * investigate the missed callback (likely a network failure on the
- * client side).
- * - If the gateway has no record of the order id, we cancel the
- * Bagisto order via OrderRepository::cancel.
- *
- * This job is intentionally idempotent - it can be re-queued safely.
- */
- class ReconcilePendingPaymentJob implements ShouldQueue
- {
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public int $tries = 3;
- public int $backoff = 60;
- public function __construct(
- public int $orderId,
- ) {}
- public function handle(OrderRepository $orderRepository): void
- {
- /** @var Order|null $order */
- $order = $orderRepository->find($this->orderId);
- if (! $order) {
- return;
- }
-
- if (! in_array($order->status, [Order::STATUS_PENDING, Order::STATUS_PENDING_PAYMENT], true)) {
- return;
- }
- $additional = $order->payment?->additional ?? [];
- $gatewayOrderId = $additional['gateway_order_id'] ?? $additional['paypal_order_id'] ?? null;
- $method = $order->payment?->method;
- if (! $gatewayOrderId || $method !== 'paypal_smart_button') {
- Event::dispatch('bagistoapi.payment.reconcile.no-gateway-info', $order);
- return;
- }
- try {
- $gatewayOrder = app(SmartButton::class)->getOrder($gatewayOrderId);
- $status = (string) ($gatewayOrder->result->status ?? '');
- $captures = $gatewayOrder->result->purchase_units[0]->payments->captures ?? [];
- if (! empty($captures) || in_array(strtoupper($status), ['COMPLETED', 'CAPTURED'], true)) {
- /*
- * Capture exists but Bagisto is still pending - a
- * callback was missed. Surface this for ops review.
- */
- Event::dispatch('bagistoapi.payment.reconcile.captured', [
- 'order' => $order,
- 'gateway_status' => $status,
- 'gateway_order' => $gatewayOrder->result ?? null,
- ]);
- return;
- }
- if (in_array(strtoupper($status), ['VOIDED', 'EXPIRED', 'PAYER_ACTION_REQUIRED'], true)) {
- Event::dispatch('bagistoapi.payment.reconcile.voided', $order);
- }
- $cancelled = DB::transaction(function () use ($orderRepository, $order, $gatewayOrderId): bool {
- /*
- * Re-lock and re-check inside a transaction right before
- * cancellation to avoid racing with a late success callback.
- */
- $orderModelClass = OrderProxy::modelClass();
- $lockedOrder = $orderModelClass::query()->whereKey($order->id)->lockForUpdate()->first();
- if (! $lockedOrder) {
- return false;
- }
- if (! in_array($lockedOrder->status, [Order::STATUS_PENDING, Order::STATUS_PENDING_PAYMENT], true)) {
- return false;
- }
- $lockedAdditional = $lockedOrder->payment?->additional ?? [];
- $lockedGatewayOrderId = $lockedAdditional['gateway_order_id'] ?? $lockedAdditional['paypal_order_id'] ?? null;
- if (! $lockedGatewayOrderId || $lockedGatewayOrderId !== $gatewayOrderId) {
- return false;
- }
- return (bool) $orderRepository->cancel($lockedOrder,true);
- });
- if ($cancelled) {
- Event::dispatch('bagistoapi.payment.reconcile.cancelled', $order);
- }
- } catch (\Throwable $e) {
- Log::warning('ReconcilePendingPaymentJob: gateway lookup failed', [
- 'order_id' => $order->id,
- 'gateway_order_id' => $gatewayOrderId,
- 'error' => $e->getMessage(),
- ]);
- /*
- * The gateway didn't tell us anything useful. Let the
- * retry mechanism take over - we'd rather try again than
- * cancel a possibly-paid order.
- */
- throw $e;
- }
- }
- }
|