|
@@ -811,6 +811,10 @@ class PaymentService
|
|
|
* amount, fill in express addresses, flip the order to processing
|
|
* amount, fill in express addresses, flip the order to processing
|
|
|
* and generate the invoice + settled transaction record.
|
|
* and generate the invoice + settled transaction record.
|
|
|
*
|
|
*
|
|
|
|
|
+ * Only a capture we can verify flips the order to processing. When the
|
|
|
|
|
+ * gateway settles asynchronously (webhook) instead, the callback is
|
|
|
|
|
+ * reported as awaiting confirmation and the order stays pending.
|
|
|
|
|
+ *
|
|
|
* Idempotent: a second (possibly concurrent) success callback for an
|
|
* Idempotent: a second (possibly concurrent) success callback for an
|
|
|
* order that is already past pending short-circuits to success
|
|
* order that is already past pending short-circuits to success
|
|
|
* without re-capturing. Concurrency is serialized via a row lock.
|
|
* without re-capturing. Concurrency is serialized via a row lock.
|
|
@@ -826,6 +830,21 @@ class PaymentService
|
|
|
$method = $order->payment?->method;
|
|
$method = $order->payment?->method;
|
|
|
$gatewayOrderId = $input->gatewayOrderId ?: ($additional['paypal_order_id'] ?? null);
|
|
$gatewayOrderId = $input->gatewayOrderId ?: ($additional['paypal_order_id'] ?? null);
|
|
|
|
|
|
|
|
|
|
+ if (! $this->canVerifyCapture($method, $order, $input)) {
|
|
|
|
|
+ Log::warning('Payment success callback could not be verified; order left pending', [
|
|
|
|
|
+ 'order_id' => $order->id,
|
|
|
|
|
+ 'payment_method' => $method,
|
|
|
|
|
+ 'gateway_order_id' => $gatewayOrderId,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ Event::dispatch('bagistoapi.payment.callback-unverified', [
|
|
|
|
|
+ 'order' => $order,
|
|
|
|
|
+ 'gateway_status' => 'awaiting-confirmation',
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return $this->awaitingConfirmationResponse($order, 'awaiting-confirmation');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
$result = DB::transaction(function () use ($order, $input, $isExpress, $method, $gatewayOrderId) {
|
|
$result = DB::transaction(function () use ($order, $input, $isExpress, $method, $gatewayOrderId) {
|
|
|
/*
|
|
/*
|
|
@@ -841,19 +860,16 @@ class PaymentService
|
|
|
if (! $this->isPending($order)) {
|
|
if (! $this->isPending($order)) {
|
|
|
return ['skipped' => true, 'capture' => null, 'response' => $this->successResponse($order, 'already_processed')];
|
|
return ['skipped' => true, 'capture' => null, 'response' => $this->successResponse($order, 'already_processed')];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
$capture = null;
|
|
$capture = null;
|
|
|
$gatewayHandler = $this->resolveGatewayHandler($method, $order, $input);
|
|
$gatewayHandler = $this->resolveGatewayHandler($method, $order, $input);
|
|
|
- if ($gatewayHandler) {
|
|
|
|
|
|
|
+ if ($gatewayHandler && method_exists($gatewayHandler, 'captureAndVerify')) {
|
|
|
$capture = $gatewayHandler->captureAndVerify($order, $gatewayOrderId);
|
|
$capture = $gatewayHandler->captureAndVerify($order, $gatewayOrderId);
|
|
|
- } else {
|
|
|
|
|
- if ($method === 'paypal_smart_button') {
|
|
|
|
|
- if (! $gatewayOrderId) {
|
|
|
|
|
- throw new OperationFailedException(__('bagistoapi::app.graphql.payment.gateway-order-id-required'));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- $capture = $this->captureAndVerify($order, $gatewayOrderId);
|
|
|
|
|
|
|
+ } elseif ($method === 'paypal_smart_button') {
|
|
|
|
|
+ if (! $gatewayOrderId) {
|
|
|
|
|
+ throw new OperationFailedException(__('bagistoapi::app.graphql.payment.gateway-order-id-required'));
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ $capture = $this->captureAndVerify($order, $gatewayOrderId);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return $this->finalizeCapturedOrder($order, $capture, $isExpress ? $input : null);
|
|
return $this->finalizeCapturedOrder($order, $capture, $isExpress ? $input : null);
|
|
@@ -1171,6 +1187,27 @@ class PaymentService
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ protected function canVerifyCapture(?string $method, $order, $input = null): bool
|
|
|
|
|
+ {
|
|
|
|
|
+ $gatewayHandler = $this->resolveGatewayHandler((string) $method, $order, $input);
|
|
|
|
|
+
|
|
|
|
|
+ if ($gatewayHandler) {
|
|
|
|
|
+ return method_exists($gatewayHandler, 'captureAndVerify');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $method === 'paypal_smart_button';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function awaitingConfirmationResponse($order, string $gatewayStatus): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'order' => $order,
|
|
|
|
|
+ 'status' => 'pending',
|
|
|
|
|
+ 'gatewayStatus' => $gatewayStatus,
|
|
|
|
|
+ 'message' => __('bagistoapi::app.graphql.payment.awaiting-confirmation'),
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
protected function gatewayOrderIdFromOrder($order): ?string
|
|
protected function gatewayOrderIdFromOrder($order): ?string
|
|
|
{
|
|
{
|
|
|
$additional = $order->payment?->additional ?? [];
|
|
$additional = $order->payment?->additional ?? [];
|