From 3a15a3029bbb07ac7a5d73039ab0b0f05da61b6d Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Tue, 28 Jan 2025 11:16:51 +0100 Subject: [PATCH 1/3] feat: Use async payment request for POS (fixing 2867) --- ...sCloudSync.php => TransactionPosCloud.php} | 12 ++- Gateway/Request/PosCloudBuilder.php | 76 +++++++++++++------ Gateway/Response/PaymentPosCloudHandler.php | 27 ++++++- .../Validator/PosCloudResponseValidator.php | 20 ++++- Model/Api/AdyenPosCloud.php | 8 ++ etc/di.xml | 4 +- 6 files changed, 114 insertions(+), 33 deletions(-) rename Gateway/Http/Client/{TransactionPosCloudSync.php => TransactionPosCloud.php} (81%) 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 32896b3ff2..9aa7f2536e 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 @@ -1780,7 +1780,7 @@ Magento\Checkout\Model\Session\Proxy - + Magento\Checkout\Model\Session\Proxy From 178b4a4130db8ed6910870268e9e22cc6037baf0 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Tue, 28 Jan 2025 14:11:02 +0100 Subject: [PATCH 2/3] fix: Use localized exception --- Model/Api/AdyenPosCloud.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Model/Api/AdyenPosCloud.php b/Model/Api/AdyenPosCloud.php index 306d14932f..991a3a1e8c 100644 --- a/Model/Api/AdyenPosCloud.php +++ b/Model/Api/AdyenPosCloud.php @@ -15,6 +15,7 @@ use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\Sales\OrderRepository; use Exception; +use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Command\CommandPoolInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Payment\Gateway\Data\PaymentDataObjectFactoryInterface; @@ -57,6 +58,6 @@ protected function execute(OrderInterface $order): void // Pending POS payment, add a short delay to avoid a flood of requests sleep(2); - throw new Exception('In Progress'); + throw new LocalizedException(__('Status: %1', 'In Progress')); } } From 5bf4e3cb684b2561ec568089622358e007a2fc51 Mon Sep 17 00:00:00 2001 From: Pieter Zandbergen Date: Tue, 28 Jan 2025 14:38:23 +0100 Subject: [PATCH 3/3] fix: Logging --- Gateway/Http/Client/TransactionPosCloud.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gateway/Http/Client/TransactionPosCloud.php b/Gateway/Http/Client/TransactionPosCloud.php index e4072306f2..2621098e67 100644 --- a/Gateway/Http/Client/TransactionPosCloud.php +++ b/Gateway/Http/Client/TransactionPosCloud.php @@ -60,14 +60,15 @@ public function placeRequest(TransferInterface $transferObject): array $request = $transferObject->getBody(); $service = $this->adyenHelper->createAdyenPosPaymentService($this->client); - $this->adyenHelper->logRequest($request, '', '/async'); + // Use async for payment requests + $sync = empty($request['SaleToPOIRequest']['PaymentRequest']); + $this->adyenHelper->logRequest($request, '', $sync ? '/sync' : '/async'); try { - if (!empty($request['SaleToPOIRequest']['PaymentRequest'])) { - // Use async for payment requests + if ($sync) { + $response = $service->runTenderSync($request); + } else { // 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