diff --git a/Gateway/Http/Client/TransactionPosCloudSync.php b/Gateway/Http/Client/TransactionPosCloud.php similarity index 81% rename from Gateway/Http/Client/TransactionPosCloudSync.php rename to Gateway/Http/Client/TransactionPosCloud.php index fadc72578c..e4072306f2 100644 --- a/Gateway/Http/Client/TransactionPosCloudSync.php +++ b/Gateway/Http/Client/TransactionPosCloud.php @@ -21,7 +21,7 @@ use Magento\Payment\Gateway\Http\TransferInterface; use Magento\Store\Model\StoreManagerInterface; -class TransactionPosCloudSync implements ClientInterface +class TransactionPosCloud implements ClientInterface { protected int $storeId; protected mixed $timeout; @@ -60,9 +60,15 @@ public function placeRequest(TransferInterface $transferObject): array $request = $transferObject->getBody(); $service = $this->adyenHelper->createAdyenPosPaymentService($this->client); - $this->adyenHelper->logRequest($request, '', '/sync'); + $this->adyenHelper->logRequest($request, '', '/async'); try { - $response = $service->runTenderSync($request); + if (!empty($request['SaleToPOIRequest']['PaymentRequest'])) { + // Use async for payment requests + // Note: Async requests do not have a response + $response = $service->runTenderAsync($request) ?? ['async' => true]; + } else { + $response = $service->runTenderSync($request); + } } catch (AdyenException $e) { //Not able to perform a payment $this->adyenLogger->addAdyenDebug($response['error'] = $e->getMessage()); diff --git a/Gateway/Request/PosCloudBuilder.php b/Gateway/Request/PosCloudBuilder.php index cbbe92aeea..0b09025611 100644 --- a/Gateway/Request/PosCloudBuilder.php +++ b/Gateway/Request/PosCloudBuilder.php @@ -18,7 +18,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; -use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; class PosCloudBuilder implements BuilderInterface { @@ -33,27 +33,6 @@ public function __construct(ChargedCurrency $chargedCurrency, PointOfSale $point public function build(array $buildSubject): array { - $paymentDataObject = SubjectReader::readPayment($buildSubject); - - $payment = $paymentDataObject->getPayment(); - $order = $payment->getOrder(); - - $request['body'] = $this->buildPosRequest( - $order, - $payment->getAdditionalInformation('terminal_id'), - $payment->getAdditionalInformation('funding_source'), - $payment->getAdditionalInformation('number_of_installments'), - ); - - return $request; - } - - private function buildPosRequest( - Order $order, - string $terminalId, - ?string $fundingSource, - ?string $numberOfInstallments - ): array { // Validate JSON that has just been parsed if it was in a valid format if (json_last_error() !== JSON_ERROR_NONE) { throw new LocalizedException( @@ -61,11 +40,53 @@ private function buildPosRequest( ); } - $poiId = $terminalId; + $paymentDataObject = SubjectReader::readPayment($buildSubject); + $payment = $paymentDataObject->getPayment(); + if (!$payment instanceof Payment) { + throw new LocalizedException(__('Expecting instance of ' . Payment::class)); + } + if ($payment->hasAdditionalInformation('pos_request')) { + // POS status request + $request['body'] = $this->buildPosStatusRequest($payment); + } else { + // New POS request + $request['body'] = $this->buildPosRequest($payment); + $payment->setAdditionalInformation('pos_request', $payment->getAdditionalInformation('terminal_id')); + } + + return $request; + } + + private function buildPosStatusRequest(Payment $payment): array { + $request = [ + 'SaleToPOIRequest' => [ + 'MessageHeader' => [ + 'MessageType' => 'Request', + 'MessageClass' => 'Service', + 'MessageCategory' => 'TransactionStatus', + 'SaleID' => 'Magento2Cloud', + 'POIID' => $payment->getAdditionalInformation('pos_request'), + 'ProtocolVersion' => '3.0', + 'ServiceID' => $this->getServiceId($payment), + ], + 'TransactionStatusRequest' => [ + 'ReceiptReprintFlag' => false, + ], + ], + ]; + $request['SaleToPOIRequest']['MessageHeader']['MessageCategory'] = 'TransactionStatus'; + + return $request; + } + + private function buildPosRequest(Payment $payment) { + $order = $payment->getOrder(); + $poiId = $payment->getAdditionalInformation('terminal_id'); + $fundingSource = $payment->getAdditionalInformation('funding_source'); + $numberOfInstallments = $payment->getAdditionalInformation('number_of_installments'); $transactionType = \Adyen\TransactionType::NORMAL; $amountCurrency = $this->chargedCurrency->getOrderAmountCurrency($order); - $serviceID = date("dHis"); $timeStamper = date("Y-m-d") . "T" . date("H:i:s+00:00"); $request = [ @@ -79,7 +100,7 @@ private function buildPosRequest( 'SaleID' => 'Magento2Cloud', 'POIID' => $poiId, 'ProtocolVersion' => '3.0', - 'ServiceID' => $serviceID + 'ServiceID' => $this->getServiceId($payment), ], 'PaymentRequest' => [ @@ -137,4 +158,9 @@ private function buildPosRequest( return $this->pointOfSale->addSaleToAcquirerData($request, $order); } + + private function getServiceId(Payment $payment): string + { + return substr(sha1(uniqid($payment->getOrder()->getIncrementId(), true)), 0, 10); + } } diff --git a/Gateway/Response/PaymentPosCloudHandler.php b/Gateway/Response/PaymentPosCloudHandler.php index 1fc761ad8f..3a1ddd175c 100644 --- a/Gateway/Response/PaymentPosCloudHandler.php +++ b/Gateway/Response/PaymentPosCloudHandler.php @@ -43,10 +43,30 @@ public function __construct( public function handle(array $handlingSubject, array $response) { - $paymentResponse = $response['SaleToPOIResponse']['PaymentResponse']; $paymentDataObject = SubjectReader::readPayment($handlingSubject); - $payment = $paymentDataObject->getPayment(); + if (!empty($response['async'])) { + // Async payment request, save Order + $order = $payment->getOrder(); + $message = __('Pos payment initiated'); + $order->addCommentToStatusHistory($message); + $order->save(); + + return; + } + + $errorCondition = $response + ['SaleToPOIResponse'] + ['TransactionStatusResponse'] + ['Response'] + ['ErrorCondition'] ?? null; + if ($errorCondition === 'InProgress') { + // Payment in progress + return; + } + + $paymentResponse = $response['SaleToPOIResponse']['PaymentResponse'] + ?? $response['SaleToPOIResponse']['TransactionStatusResponse']['RepeatedMessageResponse']['RepeatedResponseMessageBody']['PaymentResponse']; // set transaction not to processing by default wait for notification $payment->setIsTransactionPending(true); @@ -93,6 +113,9 @@ public function handle(array $handlingSubject, array $response) $payment->setIsTransactionClosed(false); $payment->setShouldCloseParentTransaction(false); + // Transaction is final + $payment->unsAdditionalInformation('pos_request'); + if ($resultCode === PaymentResponseHandler::POS_SUCCESS) { $order = $payment->getOrder(); $status = $this->statusResolver->getOrderStatusByState( diff --git a/Gateway/Validator/PosCloudResponseValidator.php b/Gateway/Validator/PosCloudResponseValidator.php index c9acbbfebe..b9cb8fc302 100644 --- a/Gateway/Validator/PosCloudResponseValidator.php +++ b/Gateway/Validator/PosCloudResponseValidator.php @@ -43,6 +43,23 @@ public function validate(array $validationSubject): ResultInterface $this->adyenLogger->addAdyenDebug(json_encode($response)); + // Do not validate (async) payment requests + if (!empty($response['async'])) { + // Async payment request + return $this->createResult(true, []); + } + + // Do not validate in progress status response + $errorCondition = $response + ['SaleToPOIResponse'] + ['TransactionStatusResponse'] + ['Response'] + ['ErrorCondition'] ?? null; + if ($errorCondition === 'InProgress') { + // Payment in progress + return $this->createResult(true, []); + } + // Check for errors if (!empty($response['error'])) { if (!empty($response['code']) && $response['code'] == CURLE_OPERATION_TIMEOUTED) { @@ -54,7 +71,8 @@ public function validate(array $validationSubject): ResultInterface } } else { // We have a paymentResponse from the terminal - $paymentResponse = $response['SaleToPOIResponse']['PaymentResponse']; + $paymentResponse = $response['SaleToPOIResponse']['PaymentResponse'] + ?? $response['SaleToPOIResponse']['TransactionStatusResponse']['RepeatedMessageResponse']['RepeatedResponseMessageBody']['PaymentResponse']; } if (!empty($paymentResponse) && $paymentResponse['Response']['Result'] != 'Success') { diff --git a/Model/Api/AdyenPosCloud.php b/Model/Api/AdyenPosCloud.php index 2e546b6f50..306d14932f 100644 --- a/Model/Api/AdyenPosCloud.php +++ b/Model/Api/AdyenPosCloud.php @@ -14,6 +14,7 @@ use Adyen\Payment\Api\AdyenPosCloudInterface; use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\Sales\OrderRepository; +use Exception; use Magento\Payment\Gateway\Command\CommandPoolInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Payment\Gateway\Data\PaymentDataObjectFactoryInterface; @@ -50,5 +51,12 @@ protected function execute(OrderInterface $order): void $paymentDataObject = $this->paymentDataObjectFactory->create($payment); $posCommand = $this->commandPool->get('authorize'); $posCommand->execute(['payment' => $paymentDataObject]); + if (!$payment->hasAdditionalInformation('pos_request')) { + return; + } + + // Pending POS payment, add a short delay to avoid a flood of requests + sleep(2); + throw new Exception('In Progress'); } } diff --git a/etc/di.xml b/etc/di.xml index e7fc019f4f..5c65ea37f1 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -991,7 +991,7 @@ AdyenPaymentPosCloudAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory - Adyen\Payment\Gateway\Http\Client\TransactionPosCloudSync + Adyen\Payment\Gateway\Http\Client\TransactionPosCloud PosCloudResponseValidator AdyenPaymentPosCloudResponseHandlerComposite @@ -1789,7 +1789,7 @@ Magento\Checkout\Model\Session\Proxy - + Magento\Checkout\Model\Session\Proxy diff --git a/view/frontend/web/js/model/adyen-payment-service.js b/view/frontend/web/js/model/adyen-payment-service.js index 3b246a1080..cbe1baabd3 100644 --- a/view/frontend/web/js/model/adyen-payment-service.js +++ b/view/frontend/web/js/model/adyen-payment-service.js @@ -93,7 +93,7 @@ define( ); }, - paymentDetails: function(data, orderId, isMultishipping = false) { + paymentDetails: function(data, orderId, isMultishipping = false, quoteId = null) { let serviceUrl; let payload = { 'payload': JSON.stringify(data), @@ -108,7 +108,7 @@ define( } else { serviceUrl = urlBuilder.createUrl( '/adyen/guest-carts/:cartId/payments-details', { - cartId: quote.getQuoteId(), + cartId: quoteId ?? quote.getQuoteId() } ); }