diff --git a/.github/docker-compose.e2e.yml b/.github/docker-compose.e2e.yml index 15204f3d3..f5e35ca5b 100644 --- a/.github/docker-compose.e2e.yml +++ b/.github/docker-compose.e2e.yml @@ -1,7 +1,7 @@ version: '3' services: playwright: - image: mcr.microsoft.com/playwright:v1.40.1-focal + image: mcr.microsoft.com/playwright:v1.41.2-focal shm_size: 1gb ipc: host cap_add: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bd9bfa328..841e1a1a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: MAGENTO_PASSWORD: ${{ secrets.MAGENTO_PASSWORD }} - name: Code Sniffer - run: vendor/bin/phpcs --extensions=php,phtml --error-severity=10 --ignore-annotations + run: vendor/bin/phpcs . - name: Run PHPUnit run: vendor/bin/phpunit --coverage-clover=build/clover.xml --log-junit=build/tests-log.xml -c Test/phpunit.xml Test/Unit diff --git a/Api/TokenDeactivateInterface.php b/Api/TokenDeactivateInterface.php new file mode 100644 index 000000000..6e8d0703d --- /dev/null +++ b/Api/TokenDeactivateInterface.php @@ -0,0 +1,29 @@ + + */ + +namespace Adyen\Payment\Api; + +/** + * Interface for deactivating the recurring tokens + */ +interface TokenDeactivateInterface +{ + /** + * Deactivate a payment token. + * + * @param string $paymentToken + * @param string $paymentMethodCode + * @param int $customerId + * @return bool + */ + public function deactivateToken(string $paymentToken, string $paymentMethodCode, int $customerId): bool; +} diff --git a/Block/Checkout/Multishipping/Success.php b/Block/Checkout/Multishipping/Success.php index f647976a6..49593511a 100644 --- a/Block/Checkout/Multishipping/Success.php +++ b/Block/Checkout/Multishipping/Success.php @@ -28,13 +28,6 @@ class Success extends \Magento\Multishipping\Block\Checkout\Success { - const ACTION_REQUIRED_STATUSES = [ - PaymentResponseHandler::REDIRECT_SHOPPER, - PaymentResponseHandler::IDENTIFY_SHOPPER, - PaymentResponseHandler::CHALLENGE_SHOPPER, - PaymentResponseHandler::PENDING - ]; - /** * @var bool */ @@ -122,7 +115,7 @@ public function __construct( public function renderAction() { foreach ($this->paymentResponseEntities as $paymentResponseEntity) { - if (in_array($paymentResponseEntity['result_code'], self::ACTION_REQUIRED_STATUSES)) { + if (in_array($paymentResponseEntity['result_code'], PaymentResponseHandler::ACTION_REQUIRED_STATUSES)) { return true; } } @@ -189,7 +182,7 @@ private function setOrderInfo($orderIds) public function getIsPaymentCompleted(int $orderId) { // TODO check for all completed responses, not only Authorised, Refused, Pending or PresentToShopper - return !in_array($this->ordersInfo[$orderId]['resultCode'], self::ACTION_REQUIRED_STATUSES); + return !in_array($this->ordersInfo[$orderId]['resultCode'], PaymentResponseHandler::ACTION_REQUIRED_STATUSES); } public function getPaymentButtonLabel(int $orderId) diff --git a/Block/Form/Moto.php b/Block/Form/Moto.php index c89bc775c..ea65cc12d 100755 --- a/Block/Form/Moto.php +++ b/Block/Form/Moto.php @@ -164,7 +164,6 @@ public function getAmount() $amount = array("value" => $value, "currency" => $currenyCode); return json_encode($amount); - } catch (\Throwable $e) { $this->adyenLogger->error( 'There was an error fetching the amount for checkout component config: ' . $e->getMessage() diff --git a/Controller/Adminhtml/Support/ConfigurationSettingsForm.php b/Controller/Adminhtml/Support/ConfigurationSettingsForm.php index 0b852c0f5..5d3e034b2 100644 --- a/Controller/Adminhtml/Support/ConfigurationSettingsForm.php +++ b/Controller/Adminhtml/Support/ConfigurationSettingsForm.php @@ -74,7 +74,6 @@ public function execute() ]; $this->supportFormHelper->handleSubmit($formData, self::CONFIGURATION_SETTINGS_EMAIL_TEMPLATE); return $this->_redirect('*/*/success'); - } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('Unable to send support message. ' . $e->getMessage())); diff --git a/Controller/Adminhtml/Support/OrderProcessingForm.php b/Controller/Adminhtml/Support/OrderProcessingForm.php index 492e26e3c..40d18be6a 100644 --- a/Controller/Adminhtml/Support/OrderProcessingForm.php +++ b/Controller/Adminhtml/Support/OrderProcessingForm.php @@ -73,7 +73,6 @@ public function execute() ]; $this->supportFormHelper->handleSubmit($formData, self::ORDER_PROCESSING); return $this->_redirect('*/*/success'); - } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('Unable to send support message. ' . $e->getMessage())); diff --git a/Controller/Adminhtml/Support/OtherTopicsForm.php b/Controller/Adminhtml/Support/OtherTopicsForm.php index fe4953613..52bda65a4 100644 --- a/Controller/Adminhtml/Support/OtherTopicsForm.php +++ b/Controller/Adminhtml/Support/OtherTopicsForm.php @@ -74,7 +74,6 @@ public function execute() $this->supportFormHelper->handleSubmit($formData, self::OTHER_TOPICS); return $this->_redirect('*/*/success'); - } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('Unable to send support message. ' . $e->getMessage())); diff --git a/Controller/Return/Index.php b/Controller/Return/Index.php index 4a175fe6b..a679a6557 100755 --- a/Controller/Return/Index.php +++ b/Controller/Return/Index.php @@ -9,27 +9,22 @@ * Author: Adyen */ +// phpcs:ignore namespace Adyen\Payment\Controller\Return; -use Adyen\AdyenException; -use Adyen\Payment\Helper\Data; -use Adyen\Payment\Helper\Idempotency; +use Adyen\Payment\Helper\PaymentResponseHandler; +use Adyen\Payment\Helper\PaymentsDetails; use Adyen\Payment\Helper\Quote; use Adyen\Payment\Helper\Config; -use Adyen\Payment\Helper\StateData; -use Adyen\Payment\Helper\Vault; use Adyen\Payment\Logger\AdyenLogger; -use Adyen\Payment\Model\Notification; -use Adyen\Service\Validator\DataArrayValidator; +use Exception; use Magento\Checkout\Model\Session; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Framework\Exception\LocalizedException; -use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Sales\Model\Order; -use Magento\Sales\Model\Order\Status\HistoryFactory; use Magento\Sales\Model\OrderFactory; -use Magento\Sales\Model\ResourceModel\Order as OrderResource; use Magento\Store\Model\StoreManagerInterface; class Index extends Action @@ -58,73 +53,53 @@ class Index extends Action protected OrderFactory $orderFactory; protected Config $configHelper; protected Order $order; - protected HistoryFactory $orderHistoryFactory; protected Session $session; protected AdyenLogger $adyenLogger; protected StoreManagerInterface $storeManager; private Quote $quoteHelper; private Order\Payment $payment; - private Vault $vaultHelper; - private OrderResource $orderResourceModel; - private StateData $stateDataHelper; - private Data $adyenDataHelper; - private OrderRepositoryInterface $orderRepository; - private Idempotency $idempotencyHelper; + private PaymentsDetails $paymentsDetailsHelper; + private PaymentResponseHandler $paymentResponseHandler; public function __construct( Context $context, OrderFactory $orderFactory, - HistoryFactory $orderHistoryFactory, Session $session, AdyenLogger $adyenLogger, StoreManagerInterface $storeManager, Quote $quoteHelper, - Vault $vaultHelper, - OrderResource $orderResourceModel, - StateData $stateDataHelper, - Data $adyenDataHelper, - OrderRepositoryInterface $orderRepository, - Idempotency $idempotencyHelper, - Config $configHelper + Config $configHelper, + PaymentsDetails $paymentsDetailsHelper, + PaymentResponseHandler $paymentResponseHandler ) { parent::__construct($context); - $this->adyenDataHelper = $adyenDataHelper; $this->orderFactory = $orderFactory; - $this->orderHistoryFactory = $orderHistoryFactory; $this->session = $session; $this->adyenLogger = $adyenLogger; $this->storeManager = $storeManager; $this->quoteHelper = $quoteHelper; - $this->vaultHelper = $vaultHelper; - $this->orderResourceModel = $orderResourceModel; - $this->stateDataHelper = $stateDataHelper; - $this->orderRepository = $orderRepository; $this->configHelper = $configHelper; - $this->idempotencyHelper = $idempotencyHelper; + $this->paymentsDetailsHelper = $paymentsDetailsHelper; + $this->paymentResponseHandler = $paymentResponseHandler; } + /** + * @throws NoSuchEntityException + * @throws LocalizedException + */ public function execute(): void { - $result = false; - // Receive all params as this could be a GET or POST request - $response = $this->getRequest()->getParams(); + $redirectResponse = $this->getRequest()->getParams(); - if ($response) { - $result = $this->validateResponse($response); - $order = $this->order; - $additionalInformation = $order->getPayment()->getAdditionalInformation(); - $resultCode = isset($response['resultCode']) ? $response['resultCode'] : null; - $paymentBrandCode = $additionalInformation['brand_code'] ?? null; - if ($resultCode === 'cancelled' && $paymentBrandCode === 'svs') { - $this->adyenDataHelper->cancelOrder($order); - } + if ($redirectResponse) { + $result = $this->validateRedirectResponse($redirectResponse); // Adjust the success path, fail path, and restore quote based on if it is a multishipping quote if ( - !empty($response['merchantReference']) && - $this->quoteHelper->getIsQuoteMultiShippingWithMerchantReference($response['merchantReference']) + !empty($redirectResponse['merchantReference']) && + $this->quoteHelper->getIsQuoteMultiShippingWithMerchantReference($redirectResponse['merchantReference']) ) { $successPath = $failPath = 'multishipping/checkout/success'; $setQuoteAsActive = true; @@ -134,240 +109,86 @@ public function execute(): void $failPath = $this->configHelper->getAdyenAbstractConfigData('return_path'); $setQuoteAsActive = false; } - } else { - $this->_redirect($this->configHelper->getAdyenAbstractConfigData('return_path')); - } - - if ($result) { - $session = $this->session; - $session->getQuote()->setIsActive($setQuoteAsActive)->save(); - $paymentAction = $this->order->getPayment()->getAdditionalInformation('action'); - $brandCode = $this->order->getPayment()->getAdditionalInformation('brand_code'); - $resultCode = $this->order->getPayment()->getAdditionalInformation('resultCode'); - - // Prevent action component to redirect page again after returning to the shop - if (($brandCode == self::BRAND_CODE_DOTPAY && $resultCode == self::RESULT_CODE_RECEIVED) || - (isset($paymentAction) && $paymentAction['type'] === 'redirect') - ) { - $this->payment->unsAdditionalInformation('action'); - $this->order->save(); - } + if ($result) { + $this->session->getQuote()->setIsActive($setQuoteAsActive)->save(); - // Add OrderIncrementId to redirect parameters for headless support. - $redirectParams = $this->configHelper->getAdyenAbstractConfigData('custom_success_redirect_path') - ? ['_query' => ['utm_nooverride' => '1', 'order_increment_id' => $this->order->getIncrementId()]] - : ['_query' => ['utm_nooverride' => '1']]; - $this->_redirect($successPath, $redirectParams); - } else { - $this->adyenLogger->addAdyenResult( - sprintf( - 'Payment for order %s was unsuccessful, ' . - 'it will be cancelled when the OFFER_CLOSED notification has been processed.', - $this->order->getIncrementId() - ) - ); - $this->replaceCart($response); - $this->_redirect($failPath, ['_query' => ['utm_nooverride' => '1']]); - } - } + // Add OrderIncrementId to redirect parameters for headless support. + $redirectParams = $this->configHelper->getAdyenAbstractConfigData('custom_success_redirect_path') + ? ['_query' => ['utm_nooverride' => '1', 'order_increment_id' => $this->order->getIncrementId()]] + : ['_query' => ['utm_nooverride' => '1']]; + $this->_redirect($successPath, $redirectParams); + } else { + $this->adyenLogger->addAdyenResult( + sprintf( + 'Payment for order %s was unsuccessful, ' . + 'it will be cancelled when the OFFER_CLOSED notification has been processed.', + isset($this->order) ? $this->order->getIncrementId() : + ($redirectResponse['merchantReference'] ?? null) + ) + ); - protected function replaceCart(array $response): void - { - $this->session->restoreQuote(); + $this->session->restoreQuote(); + $this->messageManager->addError(__('Your payment failed, Please try again later')); - if (isset($response['authResult']) && $response['authResult'] == \Adyen\Payment\Model\Notification::CANCELLED) { - $this->messageManager->addError(__('You have cancelled the order. Please try again')); + $this->_redirect($failPath, ['_query' => ['utm_nooverride' => '1']]); + } } else { - $this->messageManager->addError(__('Your payment failed, Please try again later')); + $this->_redirect($this->configHelper->getAdyenAbstractConfigData('return_path')); } } - protected function validateResponse(array $response): bool + /** + * @throws LocalizedException + * @throws Exception + */ + protected function validateRedirectResponse(array $redirectResponse): bool { - $this->adyenLogger->addAdyenResult('Processing ResultUrl'); + $this->adyenLogger->addAdyenResult('Processing redirect response'); + $order = $this->getOrder($redirectResponse['merchantReference'] ?? null); - // send the payload verification payment\details request to validate the response - $response = $this->validatePayloadAndReturnResponse($response); - - $order = $this->order; - - $this->_eventManager->dispatch( - 'adyen_payment_process_resulturl_before', - [ - 'order' => $order, - 'adyen_response' => $response - ] - ); - - // Save PSP reference from the response - if (!empty($response['pspReference'])) { - $this->payment->setAdditionalInformation('pspReference', $response['pspReference']); - } - - // Handle recurring details - $this->vaultHelper->handlePaymentResponseRecurringDetails($this->payment, $response); - - // Save donation token if available in the response - if (!empty($response['donationToken'])) { - $this->payment->setAdditionalInformation('donationToken', $response['donationToken']); + try { + // Make paymentsDetails call to validate the payment + $request["details"] = $redirectResponse; + $paymentsDetailsResponse = $this->paymentsDetailsHelper->initiatePaymentDetails($order, $request); + } catch (Exception $e) { + $paymentsDetailsResponse['error'] = $e->getMessage(); } - // update the order - $result = $this->validateUpdateOrder($order, $response); - - $this->_eventManager->dispatch( - 'adyen_payment_process_resulturl_after', - [ - 'order' => $order, - 'adyen_response' => $response - ] - ); - - return $result; - } - - protected function validateUpdateOrder(Order $order, array $response): bool - { $result = false; - if (!empty($response['authResult'])) { - $authResult = $response['authResult']; - } elseif (!empty($response['resultCode'])) { - $authResult = $response['resultCode']; - } else { - // In case the result is unknown we log the request and don't update the history - $this->adyenLogger->error("Unexpected result query parameter. Response: " . json_encode($response)); - - return $result; - } - - $this->adyenLogger->addAdyenResult('Updating the order'); - - if (isset($response['paymentMethod']['brand'])) { - $paymentMethod = $response['paymentMethod']['brand']; - } - elseif (isset($response['paymentMethod']['type'])) { - $paymentMethod = $response['paymentMethod']['type']; - } - else { - $paymentMethod = ''; - } - - $pspReference = isset($response['pspReference']) ? trim((string) $response['pspReference']) : ''; - - $type = 'Adyen Result URL response:'; - $comment = __( - '%1
authResult: %2
pspReference: %3
paymentMethod: %4', - $type, - $authResult, - $pspReference, - $paymentMethod - ); - - // needed because then we need to save $order objects - $order->setAdyenResulturlEventCode($authResult); - - // Update the payment additional information with the new result code - $orderPayment = $order->getPayment(); - $orderPayment->setAdditionalInformation('resultCode', $authResult); - $this->orderResourceModel->save($order); - - switch (strtoupper((string) $authResult)) { - case Notification::AUTHORISED: - $result = true; - $this->adyenLogger->addAdyenResult('Do nothing wait for the notification'); - break; - case Notification::RECEIVED: - $result = true; - if (strpos((string) $paymentMethod, "alipay_hk") !== false) { - $result = false; - } - $this->adyenLogger->addAdyenResult('Do nothing wait for the notification'); - break; - case Notification::PENDING: - // do nothing wait for the notification - $result = true; - if (strpos((string) $paymentMethod, "bankTransfer") !== false) { - $comment .= "

Waiting for the customer to transfer the money."; - } elseif ($paymentMethod == "sepadirectdebit") { - $comment .= "

This request will be send to the bank at the end of the day."; - } else { - $comment .= "

The payment result is not confirmed (yet). -
Once the payment is authorised, the order status will be updated accordingly. -
If the order is stuck on this status, the payment can be seen as unsuccessful. -
The order can be automatically cancelled based on the OFFER_CLOSED notification. - Please contact Adyen Support to enable this."; - } - $this->adyenLogger->addAdyenResult('Do nothing wait for the notification'); - break; - case Notification::CANCELLED: - case Notification::ERROR: - $this->adyenLogger->addAdyenResult('Cancel or Hold the order on OFFER_CLOSED notification'); - $result = false; - break; - case Notification::REFUSED: - // if refused there will be a AUTHORIZATION : FALSE notification send only exception is idea - $this->adyenLogger->addAdyenResult( - 'Cancel or Hold the order on AUTHORISATION - success = false notification' + // Compare the merchant references + $merchantReference = $paymentsDetailsResponse['merchantReference'] ?? null; + if ($merchantReference) { + if ($order->getIncrementId() === $merchantReference) { + $this->order = $order; + $this->payment = $order->getPayment(); + $this->cleanUpRedirectAction(); + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $order ); - $result = false; - - if (!$order->canCancel()) { - $order->setState(Order::STATE_NEW); - $this->orderRepository->save($order); - } - $this->adyenDataHelper->cancelOrder($order); - - break; - default: - $this->adyenLogger->addAdyenResult('This event is not supported: ' . $authResult); - $result = false; - break; - } - - $history = $this->orderHistoryFactory->create() - ->setStatus($order->getStatus()) - ->setComment($comment) - ->setEntityName('order') - ->setOrder($order); - - $history->save(); - - // Cleanup state data - try { - $this->stateDataHelper->cleanQuoteStateData($order->getQuoteId(), $authResult); - } catch (\Exception $exception) { - $this->adyenLogger->error(__('Error cleaning the payment state data: %s', $exception->getMessage())); - } - - - return $result; - } - - protected function getOrder(string $incrementId = null): Order - { - if (!isset($this->order)) { - if ($incrementId !== null) { - //TODO Replace with order repository search for best practice - $this->order = $this->orderFactory->create()->loadByIncrementId($incrementId); } else { - $this->order = $this->session->getLastRealOrder(); + $this->adyenLogger->error("Wrong merchantReference was set in the query or in the session"); } + } else { + $this->adyenLogger->error("No merchantReference in the response"); } - return $this->order; + return $result; } - protected function validatePayloadAndReturnResponse(array $result): array + /** + * @throws LocalizedException + */ + private function getOrder(string $incrementId = null): Order { - $client = $this->adyenDataHelper->initializeAdyenClient($this->storeManager->getStore()->getId()); - $service = $this->adyenDataHelper->createAdyenCheckoutService($client); - - $order = $this->getOrder( - !empty($result['merchantReference']) ? $result['merchantReference'] : null - ); + if ($incrementId !== null) { + $order = $this->orderFactory->create()->loadByIncrementId($incrementId); + } else { + $order = $this->session->getLastRealOrder(); + } if (!$order->getId()) { throw new LocalizedException( @@ -375,47 +196,25 @@ protected function validatePayloadAndReturnResponse(array $result): array ); } - $this->payment = $order->getPayment(); - - $request = []; - - // filter details to match the keys - $details = $result; - // TODO build a validator class which also validates the type of the param - $details = DataArrayValidator::getArrayOnlyWithApprovedKeys($details, self::DETAILS_ALLOWED_PARAM_KEYS); - - // Remove helper params in case left in the request - $helperParams = ['isAjax', 'merchantReference']; - foreach ($helperParams as $helperParam) { - if (array_key_exists($helperParam, $details)) { - unset($details[$helperParam]); - } - } - - $request["details"] = $details; - $requestOptions['idempotencyKey'] = $this->idempotencyHelper->generateIdempotencyKey($request); - $requestOptions['headers'] = $this->adyenDataHelper->buildRequestHeaders(); + return $order; + } - try { - $response = $service->paymentsDetails($request, $requestOptions); - $responseMerchantReference = !empty($response['merchantReference']) ? $response['merchantReference'] : null; - $resultMerchantReference = !empty($result['merchantReference']) ? $result['merchantReference'] : null; - $merchantReference = $responseMerchantReference ?: $resultMerchantReference; - if ($merchantReference) { - if ($order->getIncrementId() === $merchantReference) { - $this->order = $order; - } else { - $this->adyenLogger->error("Wrong merchantReference was set in the query or in the session"); - $response['error'] = 'merchantReference mismatch'; - } - } else { - $this->adyenLogger->error("No merchantReference in the response"); - $response['error'] = 'merchantReference is missing from the response'; - } - } catch (AdyenException $e) { - $response['error'] = $e->getMessage(); + /** + * @return void + * @throws Exception + */ + private function cleanUpRedirectAction(): void + { + // Prevent action component to redirect page again after returning to the shop + $paymentAction = $this->order->getPayment()->getAdditionalInformation('action'); + $brandCode = $this->order->getPayment()->getAdditionalInformation('brand_code'); + $resultCode = $this->order->getPayment()->getAdditionalInformation('resultCode'); + + if (($brandCode == self::BRAND_CODE_DOTPAY && $resultCode == self::RESULT_CODE_RECEIVED) || + (isset($paymentAction) && $paymentAction['type'] === 'redirect') + ) { + $this->payment->unsAdditionalInformation('action'); + $this->order->save(); } - - return $response; } } diff --git a/Cron/Providers/PayByLinkExpiredPaymentOrdersProvider.php b/Cron/Providers/PayByLinkExpiredPaymentOrdersProvider.php index 4d0227675..f6eb2c477 100644 --- a/Cron/Providers/PayByLinkExpiredPaymentOrdersProvider.php +++ b/Cron/Providers/PayByLinkExpiredPaymentOrdersProvider.php @@ -88,7 +88,6 @@ protected function getExpiredOrderIds(): array $expiredOrderIds[] = $orderPayment->getParentId(); } } - } return $expiredOrderIds; } diff --git a/Gateway/Request/CheckoutDataBuilder.php b/Gateway/Request/CheckoutDataBuilder.php index 657a3b750..e7661e8b4 100644 --- a/Gateway/Request/CheckoutDataBuilder.php +++ b/Gateway/Request/CheckoutDataBuilder.php @@ -130,7 +130,6 @@ public function build(array $buildSubject): array (isset($brandCode) && $this->adyenHelper->isPaymentMethodOpenInvoiceMethod($brandCode)) || $payment->getMethod() === AdyenPayByLinkConfigProvider::CODE ) { - $openInvoiceFields = $this->openInvoiceHelper->getOpenInvoiceDataForOrder($order); $requestBody = array_merge($requestBody, $openInvoiceFields); diff --git a/Gateway/Request/RefundDataBuilder.php b/Gateway/Request/RefundDataBuilder.php index d35c2342b..76716c4ca 100644 --- a/Gateway/Request/RefundDataBuilder.php +++ b/Gateway/Request/RefundDataBuilder.php @@ -35,6 +35,7 @@ class RefundDataBuilder implements BuilderInterface private Config $configHelper; private PaymentCollectionFactory $orderPaymentCollectionFactory; private ChargedCurrency $chargedCurrency; + private OpenInvoice $openInvoiceHelper; public function __construct( Data $adyenHelper, diff --git a/Gateway/Response/PaymentPosCloudHandler.php b/Gateway/Response/PaymentPosCloudHandler.php index b697b7b3c..190ada51b 100644 --- a/Gateway/Response/PaymentPosCloudHandler.php +++ b/Gateway/Response/PaymentPosCloudHandler.php @@ -65,7 +65,6 @@ public function handle(array $handlingSubject, array $response) $payment->getOrder()->getPayment(), $paymentResponseDecoded ); - } // set transaction(status) if (!empty($paymentResponse['PaymentResult']['PaymentAcquirerData']['AcquirerTransactionID']['TransactionID'])) diff --git a/Helper/Creditmemo.php b/Helper/Creditmemo.php index beae4c247..709ca30fe 100644 --- a/Helper/Creditmemo.php +++ b/Helper/Creditmemo.php @@ -146,7 +146,6 @@ public function linkAndUpdateAdyenCreditmemos( $this->adyenCreditmemoResourceModel->save($currAdyenCreditmemo); break; } - } } } diff --git a/Helper/Order.php b/Helper/Order.php index 0109f66ee..4608cdd5b 100644 --- a/Helper/Order.php +++ b/Helper/Order.php @@ -83,6 +83,8 @@ class Order extends AbstractHelper /** @var AdyenCreditmemoHelper */ private $adyenCreditmemoHelper; + private MagentoOrder\StatusResolver $statusResolver; + public function __construct( Context $context, Builder $transactionBuilder, @@ -100,7 +102,8 @@ public function __construct( OrderPaymentCollectionFactory $adyenOrderPaymentCollectionFactory, PaymentMethods $paymentMethodsHelper, AdyenCreditMemoResourceModel $adyenCreditmemoResourceModel, - AdyenCreditmemoHelper $adyenCreditmemoHelper + AdyenCreditmemoHelper $adyenCreditmemoHelper, + MagentoOrder\StatusResolver $statusResolver ) { parent::__construct($context); $this->transactionBuilder = $transactionBuilder; @@ -119,6 +122,7 @@ public function __construct( $this->paymentMethodsHelper = $paymentMethodsHelper; $this->adyenCreditmemoResourceModel = $adyenCreditmemoResourceModel; $this->adyenCreditmemoHelper = $adyenCreditmemoHelper; + $this->statusResolver = $statusResolver; } /** @@ -370,6 +374,28 @@ public function setPrePaymentAuthorized(MagentoOrder $order): MagentoOrder return $order; } + public function setStatusOrderCreation(OrderInterface $order): OrderInterface + { + $paymentMethod = $order->getPayment()->getMethod(); + + // Fetch the default order status for order creation from the configuration. + $status = $this->configHelper->getConfigData( + 'order_status', + $paymentMethod, + $order->getStoreId() + ); + + if (is_null($status)) { + // If the configuration doesn't exist, use the default status. + $status = $this->statusResolver->getOrderStatusByState($order, MagentoOrder::STATE_NEW); + } + + $order->setStatus($status); + $order->setState(MagentoOrder::STATE_NEW); + + return $order; + } + /** * @param MagentoOrder $order * @param $ignoreHasInvoice diff --git a/Helper/PaymentResponseHandler.php b/Helper/PaymentResponseHandler.php index 2aacb02af..fe8cec13f 100644 --- a/Helper/PaymentResponseHandler.php +++ b/Helper/PaymentResponseHandler.php @@ -14,9 +14,11 @@ use Adyen\Payment\Logger\AdyenLogger; use Exception; use Magento\Framework\Exception\AlreadyExistsException; -use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Sales\Api\Data\OrderInterface; -use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Status\HistoryFactory; +use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\ResourceModel\Order; class PaymentResponseHandler @@ -35,50 +37,46 @@ class PaymentResponseHandler const VAULT = 'Magento Vault'; const POS_SUCCESS = 'Success'; + const ACTION_REQUIRED_STATUSES = [ + self::REDIRECT_SHOPPER, + self::IDENTIFY_SHOPPER, + self::CHALLENGE_SHOPPER, + self::PENDING + ]; + /** * @var AdyenLogger */ private AdyenLogger $adyenLogger; - - /** - * @var Vault - */ private Vault $vaultHelper; - - /** - * @var Order - */ private Order $orderResourceModel; - - /** - * @var Data - */ private Data $dataHelper; - - /** - * @var Quote - */ private Quote $quoteHelper; + private \Adyen\Payment\Helper\Order $orderHelper; + private OrderRepository $orderRepository; + private HistoryFactory $orderHistoryFactory; + private StateData $stateDataHelper; - /** - * @param AdyenLogger $adyenLogger - * @param Vault $vaultHelper - * @param Order $orderResourceModel - * @param Data $dataHelper - * @param Quote $quoteHelper - */ public function __construct( AdyenLogger $adyenLogger, Vault $vaultHelper, Order $orderResourceModel, Data $dataHelper, - Quote $quoteHelper + Quote $quoteHelper, + \Adyen\Payment\Helper\Order $orderHelper, + OrderRepository $orderRepository, + HistoryFactory $orderHistoryFactory, + StateData $stateDataHelper ) { $this->adyenLogger = $adyenLogger; $this->vaultHelper = $vaultHelper; $this->orderResourceModel = $orderResourceModel; $this->dataHelper = $dataHelper; $this->quoteHelper = $quoteHelper; + $this->orderHelper = $orderHelper; + $this->orderRepository = $orderRepository; + $this->orderHistoryFactory = $orderHistoryFactory; + $this->stateDataHelper = $stateDataHelper; } public function formatPaymentResponse( @@ -125,86 +123,108 @@ public function formatPaymentResponse( } /** - * @param array $paymentsResponse - * @param Payment $payment - * @param OrderInterface|null $order + * @param array $paymentsDetailsResponse + * @param OrderInterface $order * @return bool - * @throws LocalizedException * @throws AlreadyExistsException + * @throws InputException + * @throws NoSuchEntityException */ - public function handlePaymentResponse( - array $paymentsResponse, - Payment $payment, - OrderInterface $order = null - ):bool { - if (empty($paymentsResponse)) { + public function handlePaymentsDetailsResponse( + array $paymentsDetailsResponse, + OrderInterface $order + ): bool { + if (empty($paymentsDetailsResponse)) { $this->adyenLogger->error("Payment details call failed, paymentsResponse is empty"); return false; } - if (!empty($paymentsResponse['resultCode'])) { - $payment->setAdditionalInformation('resultCode', $paymentsResponse['resultCode']); + $this->adyenLogger->addAdyenResult('Updating the order'); + $payment = $order->getPayment(); + + $authResult = $paymentsDetailsResponse['authResult'] ?? $paymentsDetailsResponse['resultCode'] ?? null; + if (is_null($authResult)) { + // In case the result is unknown we log the request and don't update the history + $this->adyenLogger->error( + "Unexpected result query parameter. Response: " . json_encode($paymentsDetailsResponse) + ); + + return false; + } + + $paymentMethod = $paymentsDetailsResponse['paymentMethod']['brand'] ?? + $paymentsDetailsResponse['paymentMethod']['type'] ?? + ''; + + $pspReference = isset($paymentsDetailsResponse['pspReference']) ? + trim((string) $paymentsDetailsResponse['pspReference']) : + ''; + + $type = 'Adyen paymentsDetails response:'; + $comment = __( + '%1
authResult: %2
pspReference: %3
paymentMethod: %4', + $type, + $authResult, + $pspReference, + $paymentMethod + ); + + if (!empty($paymentsDetailsResponse['resultCode'])) { + $payment->setAdditionalInformation('resultCode', $paymentsDetailsResponse['resultCode']); } - if (!empty($paymentsResponse['action'])) { - $payment->setAdditionalInformation('action', $paymentsResponse['action']); + if (!empty($paymentsDetailsResponse['action'])) { + $payment->setAdditionalInformation('action', $paymentsDetailsResponse['action']); } - if (!empty($paymentsResponse['additionalData'])) { - $payment->setAdditionalInformation('additionalData', $paymentsResponse['additionalData']); + if (!empty($paymentsDetailsResponse['additionalData'])) { + $payment->setAdditionalInformation('additionalData', $paymentsDetailsResponse['additionalData']); } - if (!empty($paymentsResponse['pspReference'])) { - $payment->setAdditionalInformation('pspReference', $paymentsResponse['pspReference']); + if (!empty($paymentsDetailsResponse['pspReference'])) { + $payment->setAdditionalInformation('pspReference', $paymentsDetailsResponse['pspReference']); } - if (!empty($paymentsResponse['details'])) { - $payment->setAdditionalInformation('details', $paymentsResponse['details']); + if (!empty($paymentsDetailsResponse['details'])) { + $payment->setAdditionalInformation('details', $paymentsDetailsResponse['details']); } - switch ($paymentsResponse['resultCode']) { - case self::PRESENT_TO_SHOPPER: - case self::PENDING: - case self::RECEIVED: - case self::IDENTIFY_SHOPPER: - case self::CHALLENGE_SHOPPER: - break; - //We don't need to handle these resultCodes - case self::REDIRECT_SHOPPER: - $this->adyenLogger->addAdyenResult("Customer was redirected."); - if ($order) { - $order->addStatusHistoryComment( - __( - 'Customer was redirected to an external payment page. (In case of card payments the shopper is redirected to the bank for 3D-secure validation.) Once the shopper is authenticated, - the order status will be updated accordingly. -
Make sure that your notifications are being processed! -
If the order is stuck on this status, the shopper abandoned the session. - The payment can be seen as unsuccessful. -
The order can be automatically cancelled based on the OFFER_CLOSED notification. - Please contact Adyen Support to enable this.' - ), - $order->getStatus() - )->save(); - } - break; + if (!empty($paymentsDetailsResponse['donationToken'])) { + $payment->setAdditionalInformation('donationToken', $paymentsDetailsResponse['donationToken']); + } + + // Handle recurring details + $this->vaultHelper->handlePaymentResponseRecurringDetails($payment, $paymentsDetailsResponse); + + // If the response is valid, update the order status. + if (!in_array($paymentsDetailsResponse['resultCode'], PaymentResponseHandler::ACTION_REQUIRED_STATUSES)) { + /* + * Change order state from pending_payment to new and expect authorisation webhook + * if no additional action is required according to /paymentsDetails response. + * Otherwise keep the order state as pending_payment. + */ + $order = $this->orderHelper->setStatusOrderCreation($order); + $this->orderRepository->save($order); + } + + // Cleanup state data if exists. + try { + $this->stateDataHelper->cleanQuoteStateData($order->getQuoteId(), $authResult); + } catch (Exception $exception) { + $this->adyenLogger->error(__('Error cleaning the payment state data: %s', $exception->getMessage())); + } + + switch ($paymentsDetailsResponse['resultCode']) { case self::AUTHORISED: - if (!empty($paymentsResponse['pspReference'])) { + if (!empty($paymentsDetailsResponse['pspReference'])) { // set pspReference as transactionId - $payment->setCcTransId($paymentsResponse['pspReference']); - $payment->setLastTransId($paymentsResponse['pspReference']); + $payment->setCcTransId($paymentsDetailsResponse['pspReference']); + $payment->setLastTransId($paymentsDetailsResponse['pspReference']); // set transaction - $payment->setTransactionId($paymentsResponse['pspReference']); - } - - // Handle recurring details - $this->vaultHelper->handlePaymentResponseRecurringDetails($payment, $paymentsResponse); - - if (!empty($paymentsResponse['donationToken'])) { - $payment->setAdditionalInformation('donationToken', $paymentsResponse['donationToken']); + $payment->setTransactionId($paymentsDetailsResponse['pspReference']); } - $this->orderResourceModel->save($order); try { $this->quoteHelper->disableQuote($order->getQuoteId()); } catch (Exception $e) { @@ -212,8 +232,47 @@ public function handlePaymentResponse( 'quoteId' => $order->getQuoteId() ]); } + + $result = true; + break; + case self::PENDING: + /* Change order state from pending_payment to new and expect authorisation webhook + * if no additional action is required according to /paymentDetails response. */ + $order = $this->orderHelper->setStatusOrderCreation($order); + $this->orderRepository->save($order); + + // do nothing wait for the notification + if (strpos((string) $paymentMethod, "bankTransfer") !== false) { + $comment .= "

Waiting for the customer to transfer the money."; + } elseif ($paymentMethod == "sepadirectdebit") { + $comment .= "

This request will be send to the bank at the end of the day."; + } else { + $comment .= "

The payment result is not confirmed (yet). +
Once the payment is authorised, the order status will be updated accordingly. +
If the order is stuck on this status, the payment can be seen as unsuccessful. +
The order can be automatically cancelled based on the OFFER_CLOSED notification. + Please contact Adyen Support to enable this."; + } + $this->adyenLogger->addAdyenResult('Do nothing wait for the notification'); + + $result = true; + break; + case self::PRESENT_TO_SHOPPER: + case self::IDENTIFY_SHOPPER: + case self::CHALLENGE_SHOPPER: + case self::REDIRECT_SHOPPER: + $this->adyenLogger->addAdyenResult("Additional action is required for the payment."); + $result = true; + break; + case self::RECEIVED: + $result = true; + if (str_contains((string)$paymentMethod, "alipay_hk")) { + $result = false; + } + $this->adyenLogger->addAdyenResult('Do nothing wait for the notification'); break; case self::REFUSED: + case self::CANCELLED: // Cancel order in case result is refused if (null !== $order) { // Set order to new so it can be cancelled @@ -222,17 +281,32 @@ public function handlePaymentResponse( $order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_CANCEL, true); $this->dataHelper->cancelOrder($order); } - return false; - case self::ERROR: + $result = false; + break; default: $this->adyenLogger->error( - sprintf("Payment details call failed for action, resultCode is %s Raw API responds: %s", - $paymentsResponse['resultCode'], - json_encode($paymentsResponse) + sprintf("Payment details call failed for action, resultCode is %s Raw API responds: %s. + Cancel or Hold the order on OFFER_CLOSED notification.", + $paymentsDetailsResponse['resultCode'], + json_encode($paymentsDetailsResponse) )); - return false; + $result = false; + break; } - return true; + + $history = $this->orderHistoryFactory->create() + ->setStatus($order->getStatus()) + ->setComment($comment) + ->setEntityName('order') + ->setOrder($order); + + $history->save(); + + // needed because then we need to save $order objects + $order->setAdyenResulturlEventCode($authResult); + $this->orderResourceModel->save($order); + + return $result; } } diff --git a/Helper/PaymentsDetails.php b/Helper/PaymentsDetails.php index 117d77298..279708403 100644 --- a/Helper/PaymentsDetails.php +++ b/Helper/PaymentsDetails.php @@ -12,9 +12,11 @@ namespace Adyen\Payment\Helper; use Adyen\AdyenException; +use Adyen\Payment\Helper\Util\DataArrayValidator; use Adyen\Payment\Logger\AdyenLogger; -use Adyen\Service\Validator\DataArrayValidator; use Magento\Checkout\Model\Session; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\ValidatorException; use Magento\Sales\Api\Data\OrderInterface; @@ -26,87 +28,68 @@ class PaymentsDetails 'threeDSAuthenticationOnly' ]; + const REQUEST_HELPER_PARAMETERS = [ + 'isAjax', + 'merchantReference' + ]; + private Session $checkoutSession; private Data $adyenHelper; private AdyenLogger $adyenLogger; - private PaymentResponseHandler $paymentResponseHandler; private Idempotency $idempotencyHelper; public function __construct( Session $checkoutSession, Data $adyenHelper, AdyenLogger $adyenLogger, - PaymentResponseHandler $paymentResponseHandler, Idempotency $idempotencyHelper ) { $this->checkoutSession = $checkoutSession; $this->adyenHelper = $adyenHelper; $this->adyenLogger = $adyenLogger; - $this->paymentResponseHandler = $paymentResponseHandler; $this->idempotencyHelper = $idempotencyHelper; } - public function initiatePaymentDetails(OrderInterface $order, string $payload): string + /** + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws ValidatorException + */ + public function initiatePaymentDetails(OrderInterface $order, array $payload): array { - // Decode payload from frontend - $payload = json_decode($payload, true); - - // Validate JSON that has just been parsed if it was in a valid format - if (json_last_error() !== JSON_ERROR_NONE) { - throw new ValidatorException(__('Payment details call failed because the request was not a valid JSON')); - } - - $payment = $order->getPayment(); - $apiPayload = DataArrayValidator::getArrayOnlyWithApprovedKeys($payload, self::PAYMENTS_DETAILS_KEYS); + $request = $this->cleanUpPaymentDetailsPayload($payload); - // Send the request try { $client = $this->adyenHelper->initializeAdyenClient($order->getStoreId()); $service = $this->adyenHelper->createAdyenCheckoutService($client); - $requestOptions['idempotencyKey'] = $this->idempotencyHelper->generateIdempotencyKey($apiPayload); + $requestOptions['idempotencyKey'] = $this->idempotencyHelper->generateIdempotencyKey($request); $requestOptions['headers'] = $this->adyenHelper->buildRequestHeaders(); - $paymentDetails = $service->paymentsDetails($apiPayload, $requestOptions); + $response = $service->paymentsDetails($request, $requestOptions); } catch (AdyenException $e) { $this->adyenLogger->error("Payment details call failed: " . $e->getMessage()); $this->checkoutSession->restoreQuote(); - // accept cancellation request, restore quote - if (!empty($payload['cancelled'])) { - throw $this->createCancelledException(); - } else { - throw new ValidatorException(__('Payment details call failed')); - } + throw new ValidatorException(__('Payment details call failed')); } - // Handle response - if (!$this->paymentResponseHandler->handlePaymentResponse($paymentDetails, $payment, $order)) { - $this->checkoutSession->restoreQuote(); - throw new ValidatorException(__('The payment is REFUSED.')); - } + return $response; + } - $action = null; - if (!empty($paymentDetails['action'])) { - $action = $paymentDetails['action']; - } + private function cleanUpPaymentDetailsPayload(array $payload): array + { + $payload = DataArrayValidator::getArrayOnlyWithApprovedKeys( + $payload, + self::PAYMENTS_DETAILS_KEYS + ); - $additionalData = null; - if (!empty($paymentDetails['additionalData'])) { - $additionalData = $paymentDetails['additionalData']; + foreach (self::REQUEST_HELPER_PARAMETERS as $helperParam) { + if (array_key_exists($helperParam, $payload['details'])) { + unset($payload['details'][$helperParam]); + } } - return json_encode( - $this->paymentResponseHandler->formatPaymentResponse( - $paymentDetails['resultCode'], - $action, - $additionalData - ) - ); - } - - private function createCancelledException(): ValidatorException - { - return new ValidatorException(__('Payment has been cancelled')); + return $payload; } } diff --git a/Helper/Vault.php b/Helper/Vault.php index 620a75c63..6058e0b40 100644 --- a/Helper/Vault.php +++ b/Helper/Vault.php @@ -227,7 +227,7 @@ public function createVaultToken(OrderPaymentInterface $payment, string $detailR $details = [ 'type' => $payment->getCcType(), self::TOKEN_LABEL => sprintf( - "%s % %", + "%s %s %s", $paymentMethodInstance->getTitle(), __("token created on"), $today->format('Y-m-d') diff --git a/Model/Api/AdyenPaymentsDetails.php b/Model/Api/AdyenPaymentsDetails.php index 4e2d4c58b..5691a359a 100644 --- a/Model/Api/AdyenPaymentsDetails.php +++ b/Model/Api/AdyenPaymentsDetails.php @@ -12,24 +12,32 @@ namespace Adyen\Payment\Model\Api; use Adyen\Payment\Api\AdyenPaymentsDetailsInterface; +use Adyen\Payment\Helper\PaymentResponseHandler; use Adyen\Payment\Helper\PaymentsDetails; +use Magento\Checkout\Model\Session; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\ValidatorException; use Magento\Sales\Api\OrderRepositoryInterface; class AdyenPaymentsDetails implements AdyenPaymentsDetailsInterface { private OrderRepositoryInterface $orderRepository; - private PaymentsDetails $paymentsDetails; + private PaymentResponseHandler $paymentResponseHandler; + private Session $checkoutSession; public function __construct( OrderRepositoryInterface $orderRepository, - PaymentsDetails $paymentsDetails + PaymentsDetails $paymentsDetails, + PaymentResponseHandler $paymentResponseHandler, + Session $checkoutSession ) { $this->orderRepository = $orderRepository; $this->paymentsDetails = $paymentsDetails; + $this->paymentResponseHandler = $paymentResponseHandler; + $this->checkoutSession = $checkoutSession; } /** @@ -45,6 +53,30 @@ public function initiate(string $payload, string $orderId): string { $order = $this->orderRepository->get(intval($orderId)); - return $this->paymentsDetails->initiatePaymentDetails($order, $payload); + // Decode payload from frontend + $payload = json_decode($payload, true); + + // Validate JSON that has just been parsed if it was in a valid format + if (json_last_error() !== JSON_ERROR_NONE) { + throw new ValidatorException( + __('Payment details call failed because the request was not a valid JSON') + ); + } + + $response = $this->paymentsDetails->initiatePaymentDetails($order, $payload); + + // Handle response + if (!$this->paymentResponseHandler->handlePaymentsDetailsResponse($response, $order)) { + $this->checkoutSession->restoreQuote(); + throw new ValidatorException(__('The payment is REFUSED.')); + } + + return json_encode( + $this->paymentResponseHandler->formatPaymentResponse( + $response['resultCode'], + $response['action'] ?? null, + $response['additionalData'] ?? null + ) + ); } } diff --git a/Model/Api/GuestAdyenPaymentsDetails.php b/Model/Api/GuestAdyenPaymentsDetails.php index def9472b6..da426e647 100644 --- a/Model/Api/GuestAdyenPaymentsDetails.php +++ b/Model/Api/GuestAdyenPaymentsDetails.php @@ -12,7 +12,6 @@ namespace Adyen\Payment\Model\Api; use Adyen\Payment\Api\GuestAdyenPaymentsDetailsInterface; -use Adyen\Payment\Helper\PaymentsDetails; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -23,19 +22,17 @@ class GuestAdyenPaymentsDetails implements GuestAdyenPaymentsDetailsInterface { private OrderRepositoryInterface $orderRepository; - private QuoteIdMaskFactory $quoteIdMaskFactory; - - private PaymentsDetails $paymentsDetails; + private AdyenPaymentsDetails $adyenPaymentsDetails; public function __construct( OrderRepositoryInterface $orderRepository, QuoteIdMaskFactory $quoteIdMaskFactory, - PaymentsDetails $paymentsDetails + AdyenPaymentsDetails $adyenPaymentsDetails ) { $this->orderRepository = $orderRepository; $this->quoteIdMaskFactory = $quoteIdMaskFactory; - $this->paymentsDetails = $paymentsDetails; + $this->adyenPaymentsDetails = $adyenPaymentsDetails; } /** @@ -61,6 +58,6 @@ public function initiate(string $payload, string $orderId, string $cartId): stri ); } - return $this->paymentsDetails->initiatePaymentDetails($order, $payload); + return $this->adyenPaymentsDetails->initiate($payload, $orderId); } } diff --git a/Model/Api/TokenDeactivate.php b/Model/Api/TokenDeactivate.php new file mode 100644 index 000000000..264ca0fd5 --- /dev/null +++ b/Model/Api/TokenDeactivate.php @@ -0,0 +1,81 @@ + + */ + +namespace Adyen\Payment\Model\Api; + +use Adyen\Payment\Api\TokenDeactivateInterface; +use Adyen\Payment\Logger\AdyenLogger; +use Magento\Vault\Api\PaymentTokenRepositoryInterface; +use Magento\Vault\Model\PaymentTokenManagement; +use Exception; + +class TokenDeactivate implements TokenDeactivateInterface +{ + /** + * @var PaymentTokenRepositoryInterface + */ + protected PaymentTokenRepositoryInterface $paymentTokenRepository; + + /** + * @var PaymentTokenManagement + */ + protected PaymentTokenManagement $paymentTokenManagement; + + /** + * @var AdyenLogger + */ + protected AdyenLogger $adyenLogger; + + /** + * @param PaymentTokenRepositoryInterface $paymentTokenRepository + * @param PaymentTokenManagement $paymentTokenManagement + * @param AdyenLogger $adyenLogger + */ + public function __construct( + PaymentTokenRepositoryInterface $paymentTokenRepository, + PaymentTokenManagement $paymentTokenManagement, + AdyenLogger $adyenLogger + ) { + $this->paymentTokenRepository = $paymentTokenRepository; + $this->paymentTokenManagement = $paymentTokenManagement; + $this->adyenLogger = $adyenLogger; + } + + /** + * @param string $paymentToken + * @param string $paymentMethodCode + * @param int $customerId + * @return bool + */ + public function deactivateToken(string $paymentToken, string $paymentMethodCode, int $customerId): bool + { + $paymentToken = $this->paymentTokenManagement->getByGatewayToken( + $paymentToken, + $paymentMethodCode, + $customerId + ); + + if (isset($paymentToken)) { + try { + return $this->paymentTokenRepository->delete($paymentToken); + } catch (Exception $e) { + $this->adyenLogger->error(sprintf( + 'Error while attempting to deactivate token with id %s: %s', + $paymentToken->getEntityId(), + $e->getMessage() + )); + } + } + + return false; + } +} diff --git a/Observer/SetOrderStateAfterPaymentObserver.php b/Observer/SetOrderStateAfterPaymentObserver.php new file mode 100644 index 000000000..cd7509244 --- /dev/null +++ b/Observer/SetOrderStateAfterPaymentObserver.php @@ -0,0 +1,77 @@ + + */ + +namespace Adyen\Payment\Observer; + +use Adyen\Payment\Helper\PaymentResponseHandler; +use Adyen\Payment\Model\Method\Adapter; +use Exception; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\StatusResolver; + +class SetOrderStateAfterPaymentObserver implements ObserverInterface +{ + private StatusResolver $statusResolver; + + public function __construct( + StatusResolver $statusResolver + ) { + $this->statusResolver = $statusResolver; + } + + /** + * @throws LocalizedException + * @throws Exception + */ + public function execute(Observer $observer) + { + /** @var Payment $payment */ + $payment = $observer->getData('payment'); + $methodInstance = $payment->getMethodInstance(); + + if ($methodInstance instanceof Adapter) { + $order = $payment->getOrder(); + $resultCode = $payment->getAdditionalInformation('resultCode'); + $action = $payment->getAdditionalInformation('action'); + + /* + * Set order status and state to pending_payment if an addition action is required. + * This status will be changed when the shopper completes the action or returns from a redirection. + */ + if (in_array($resultCode, PaymentResponseHandler::ACTION_REQUIRED_STATUSES) && + !is_null($action) + ) { + $actionType = $action['type']; + + $status = $this->statusResolver->getOrderStatusByState( + $payment->getOrder(), + Order::STATE_PENDING_PAYMENT + ); + $order->setState(Order::STATE_PENDING_PAYMENT); + $order->setStatus($status); + + $message = sprintf( + __("%s action is required to complete the payment.
Result code: %s"), + ucfirst($actionType), + $resultCode + ); + + $order->addCommentToStatusHistory($message, $status); + $order->save(); + } + } + } +} diff --git a/Observer/SubmitQuoteObserver.php b/Observer/SubmitQuoteObserver.php index 29e38353a..96aff02d1 100755 --- a/Observer/SubmitQuoteObserver.php +++ b/Observer/SubmitQuoteObserver.php @@ -1,7 +1,18 @@ + */ namespace Adyen\Payment\Observer; +use Adyen\Payment\Helper\PaymentMethods; +use Adyen\Payment\Helper\PaymentResponseHandler; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Quote\Model\Quote; @@ -9,7 +20,13 @@ class SubmitQuoteObserver implements ObserverInterface { - const PAYMENT_COMPLETE = ['Authorised', 'Received', 'PresentToShopper']; + private PaymentMethods $paymentMethodsHelper; + + public function __construct( + PaymentMethods $paymentMethodsHelper + ) { + $this->paymentMethodsHelper = $paymentMethodsHelper; + } public function execute(Observer $observer) { @@ -18,19 +35,12 @@ public function execute(Observer $observer) /** @var Order\Payment $payment */ $payment = $order->getPayment(); - // No further shopper action required - $resultCode = $payment->getAdditionalInformation('resultCode'); - if (in_array($resultCode, self::PAYMENT_COMPLETE, true)) { - return; - } - - // Further shopper action required (e.g. redirect or 3DS authentication) - //TODO: Once we have a config in the magento backoffice, get all the methods directly from this config - if (in_array( - $payment->getMethod(), - ['adyen_hpp', 'adyen_cc', 'adyen_oneclick', 'adyen_paypal', 'adyen_ideal'], - true - )) { + if ($this->paymentMethodsHelper->isAdyenPayment($payment->getMethod()) && + in_array( + $payment->getAdditionalInformation('resultCode'), + PaymentResponseHandler::ACTION_REQUIRED_STATUSES + )) { + // Further shopper action required (e.g. redirect or 3DS authentication) /** @var Quote $quote */ $quote = $observer->getEvent()->getQuote(); // Keep cart active until such actions are taken diff --git a/Plugin/PaymentVaultDeleteToken.php b/Plugin/PaymentVaultDeleteToken.php index dd406b2cc..1712b1e02 100644 --- a/Plugin/PaymentVaultDeleteToken.php +++ b/Plugin/PaymentVaultDeleteToken.php @@ -12,6 +12,7 @@ namespace Adyen\Payment\Plugin; use Adyen\AdyenException; +use Adyen\Client; use Adyen\Payment\Helper\Data; use Adyen\Payment\Helper\Requests; use Adyen\Payment\Helper\Vault; @@ -49,7 +50,8 @@ public function __construct( * @return PaymentTokenInterface[]|void * @throws NoSuchEntityException */ - public function beforeDelete(PaymentTokenRepositoryInterface $subject, PaymentTokenInterface $paymentToken): ?array { + public function beforeDelete(PaymentTokenRepositoryInterface $subject, PaymentTokenInterface $paymentToken): ?array + { $paymentMethodCode = $paymentToken->getPaymentMethodCode(); $storeId = $this->storeManager->getStore()->getStoreId(); @@ -59,7 +61,14 @@ public function beforeDelete(PaymentTokenRepositoryInterface $subject, PaymentTo try { $client = $this->dataHelper->initializeAdyenClient($storeId); $recurringService = $this->dataHelper->createAdyenRecurringService($client); - $recurringService->disable($request); + + $this->dataHelper->logRequest( + $request, + Client::API_RECURRING_VERSION, + sprintf("/pal/servlet/Recurring/%s/disable", Client::API_RECURRING_VERSION) + ); + $response = $recurringService->disable($request); + $this->dataHelper->logResponse($response); } catch (AdyenException $e) { $this->adyenLogger->error(sprintf( 'Error while attempting to disable token with id %s: %s', diff --git a/Test/Unit/Controller/Return/IndexTest.php b/Test/Unit/Controller/Return/IndexTest.php new file mode 100644 index 000000000..bdad75ca4 --- /dev/null +++ b/Test/Unit/Controller/Return/IndexTest.php @@ -0,0 +1,267 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Controller\Return; + +use Adyen\Payment\Controller\Return\Index; +use Adyen\Payment\Helper\Config; +use Adyen\Payment\Helper\PaymentResponseHandler; +use Adyen\Payment\Helper\PaymentsDetails; +use Adyen\Payment\Helper\Quote; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Exception; +use Magento\Checkout\Model\Session; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\ManagerInterface as MessageManagerInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\OrderFactory; +use Magento\Store\Model\StoreManagerInterface; + +class IndexTest extends AbstractAdyenTestCase +{ + private $indexControllerMock; + private $controllerRequestMock; + private $messageManagerMock; + private $redirectMock; + private $contextResponseMock; + private $quoteMock; + private $orderEntityMock; + private $paymentEntityMock; + + private $contextMock; + private $orderFactoryMock; + private $sessionMock; + private $adyenLoggerMock; + private $storeManagerMock; + private $quoteHelperMock; + private $configHelperMock; + private $paymentsDetailsHelperMock; + private $paymentResponseHandlerMock; + + protected function setUp(): void + { + // Constructor argument mocks + $this->contextMock = $this->createMock(Context::class); + $this->orderFactoryMock = $this->createGeneratedMock(OrderFactory::class, ['create']); + $this->sessionMock = $this->createMock(Session::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->quoteHelperMock = $this->createMock(Quote::class); + $this->configHelperMock = $this->createMock(Config::class); + $this->paymentsDetailsHelperMock = $this->createMock(PaymentsDetails::class); + $this->paymentResponseHandlerMock = $this->createMock(PaymentResponseHandler::class); + + // Extra mock objects and methods + $this->messageManagerMock = $this->createMock(MessageManagerInterface::class); + $this->redirectMock = $this->createMock(RedirectInterface::class); + $this->contextResponseMock = $this->createMock(ResponseInterface::class); + $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); + $this->paymentEntityMock = $this->createMock(Payment::class); + $this->paymentEntityMock->method('getAdditionalInformation')->will( + $this->returnValueMap([ + ['action', ['type' => 'redirect']], + ['brand_code', Index::BRAND_CODE_DOTPAY], + ['resultCode', Index::RESULT_CODE_RECEIVED] + ]) + ); + $this->orderEntityMock = $this->createMock(Order::class); + $this->orderEntityMock->method('getPayment')->willReturn($this->paymentEntityMock); + $this->controllerRequestMock = $this->createMock(RequestInterface::class); + $this->orderFactoryMock->method('create')->willReturn($this->orderEntityMock); + $this->orderEntityMock->method('loadByIncrementId')->willReturnSelf(); + $this->quoteMock->method('setIsActive')->willReturnSelf(); + $this->sessionMock->method('getLastRealOrder')->willReturn($this->orderEntityMock); + $this->sessionMock->method('getQuote')->willReturn($this->quoteMock); + $this->contextMock->method('getRedirect')->willReturn($this->redirectMock); + $this->contextMock->method('getRequest')->willReturn($this->controllerRequestMock); + $this->contextMock->method('getMessageManager')->willReturn($this->messageManagerMock); + $this->contextMock->method('getResponse')->willReturn($this->contextResponseMock); + $this->configHelperMock->method('getAdyenAbstractConfigData')->will( + $this->returnValueMap([ + ['return_path', null, '/checkout/cart'], + ['custom_success_redirect_path', null, null] + ]) + ); + + $this->indexControllerMock = new Index( + $this->contextMock, + $this->orderFactoryMock, + $this->sessionMock, + $this->adyenLoggerMock, + $this->storeManagerMock, + $this->quoteHelperMock, + $this->configHelperMock, + $this->paymentsDetailsHelperMock, + $this->paymentResponseHandlerMock + ); + } + + private static function testDataProvider(): array + { + return [ + [ + 'redirectResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'redirectResult' => 'ABCDEFG123456789' + ], + 'paymentsDetailsResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'resultCode' => 'Authorised' + ], + 'responseHandlerResult' => true, + 'returnPath' => 'checkout/onepage/success', + 'orderId' => PHP_INT_MAX, + 'expectedException' => null + ], + [ + 'redirectResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'redirectResult' => 'ABCDEFG123456789' + ], + 'paymentsDetailsResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'resultCode' => 'Authorised' + ], + 'responseHandlerResult' => true, + 'returnPath' => 'multishipping/checkout/success', + 'orderId' => PHP_INT_MAX, + 'expectedException' => null, + 'multishipping' => true + ], + [ + 'redirectResponse' => [ + 'redirectResult' => 'ABCDEFG123456789' + ], + 'paymentsDetailsResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'resultCode' => 'Authorised' + ], + 'responseHandlerResult' => true, + 'returnPath' => 'checkout/onepage/success', + 'orderId' => PHP_INT_MAX, + 'expectedException' => null + ], + [ + 'redirectResponse' => [ + 'merchantReference' => PHP_INT_MIN, + 'redirectResult' => 'ABCDEFG123456789' + ], + 'paymentsDetailsResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'resultCode' => 'Authorised' + ], + 'responseHandlerResult' => false, + 'returnPath' => null, + 'orderId' => null, + 'expectedException' => LocalizedException::class + ], + [ + 'redirectResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'redirectResult' => 'ABCDEFG123456789' + ], + 'paymentsDetailsResponse' => [], + 'responseHandlerResult' => false, + 'returnPath' => '/checkout/cart', + 'orderId' => PHP_INT_MAX, + 'expectedException' => null + ], + [ + 'redirectResponse' => [ + 'merchantReference' => PHP_INT_MIN, + 'redirectResult' => 'ABCDEFG123456789' + ], + 'paymentsDetailsResponse' => [ + 'merchantReference' => PHP_INT_MAX, + 'resultCode' => 'Authorised' + ], + 'responseHandlerResult' => false, + 'returnPath' => '/checkout/cart', + 'orderId' => PHP_INT_MIN, + 'expectedException' => null + ], + [ + 'redirectResponse' => [ + 'merchantReference' => PHP_INT_MIN, + 'redirectResult' => 'ABCDEFG123456789' + ], + 'paymentsDetailsResponse' => [ + 'merchantReference' => null, + 'resultCode' => null + ], + 'responseHandlerResult' => false, + 'returnPath' => '/checkout/cart', + 'orderId' => PHP_INT_MIN, + 'expectedException' => null + ], + [ + 'redirectResponse' => null, + 'paymentsDetailsResponse' => [ + 'merchantReference' => null, + 'resultCode' => null + ], + 'responseHandlerResult' => false, + 'returnPath' => '/checkout/cart', + 'orderId' => PHP_INT_MIN, + 'expectedException' => null + ] + ]; + } + + /** + * @dataProvider testDataProvider + */ + public function testExecute( + $redirectResponse, + $paymentsDetailsResponse, + $responseHandlerResult, + $returnPath, + $orderId, + $expectedException, + $multishipping = false + ) { + if ($expectedException) { + $this->expectException($expectedException); + } else { + $this->redirectMock->expects($this->once())->method('redirect')->with( + $this->contextResponseMock, + $returnPath, + $redirectResponse ? ['_query' => ['utm_nooverride' => '1']] : [] + ); + } + + if ($multishipping) { + $this->quoteHelperMock->method('getIsQuoteMultiShippingWithMerchantReference') + ->willReturn(true); + } + + if (empty($paymentsDetailsResponse)) { + $this->paymentsDetailsHelperMock->method('initiatePaymentDetails') + ->willThrowException(new Exception); + } + + $this->controllerRequestMock->method('getParams')->willReturn($redirectResponse); + $this->orderEntityMock->method('getId')->willReturn($orderId); + $this->orderEntityMock->method('getIncrementId')->willReturn($orderId); + $this->paymentResponseHandlerMock->method('handlePaymentsDetailsResponse') + ->willReturn($responseHandlerResult); + $this->paymentsDetailsHelperMock->method('initiatePaymentDetails') + ->willReturn($paymentsDetailsResponse); + + $this->indexControllerMock->execute(); + } +} diff --git a/Test/Unit/Helper/OrderTest.php b/Test/Unit/Helper/OrderTest.php index 122b43ae5..35527dca3 100644 --- a/Test/Unit/Helper/OrderTest.php +++ b/Test/Unit/Helper/OrderTest.php @@ -30,11 +30,13 @@ use Magento\Framework\App\Helper\Context; use Magento\Framework\DB\TransactionFactory; use Magento\Framework\Notification\NotifierPool; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order as MagentoOrder; use Magento\Sales\Model\Order\Email\Sender\OrderSender; use Magento\Sales\Model\Order\Payment\Transaction\Builder; use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\ResourceModel\Order\Status\CollectionFactory as OrderStatusCollectionFactory; +use Magento\Sales\Api\Data\TransactionInterface; class OrderTest extends AbstractAdyenTestCase { @@ -152,6 +154,50 @@ public function testHoldCancelOrderNotCancellable() $orderHelper->holdCancelOrder($order, false); } + public function testHoldCancelOrderNotConfigurableToCancel() + { + $storeId = 1; + $configHelper = $this->createMock(Config::class); + $configHelper->method('getNotificationsCanCancel') + ->with($storeId) + ->willReturn(false); + + $adyenLoggerMock = $this->createMock(AdyenLogger::class); + $adyenLoggerMock->expects($this->once()) + ->method('addAdyenNotification') + ->with( + $this->stringContains('Order cannot be cancelled based on the plugin configuration'), + $this->arrayHasKey('pspReference') + ); + + $paymentMock = $this->createMock(\Magento\Sales\Model\Order\Payment::class); + $paymentMock->method('getData')->willReturnMap([ + ['adyen_psp_reference', null, 'test_psp_reference'], + ['entity_id', null, 'test_entity_id'] + ]); + + $orderMock = $this->createMock(MagentoOrder::class); + $orderMock->method('getStoreId')->willReturn($storeId); + $orderMock->method('getPayment')->willReturn($paymentMock); + $orderMock->expects($this->never())->method('cancel'); + $orderMock->expects($this->never())->method('hold'); + + $orderHelper = $this->createOrderHelper( + null, + $configHelper, + null, + null, + null, + null, + null, + $adyenLoggerMock + ); + + $result = $orderHelper->holdCancelOrder($orderMock, false); + + $this->assertSame($orderMock, $result); + } + public function testRefundOrderSuccessful() { $dataHelper = $this->createPartialMock(Data::class, []); @@ -257,6 +303,387 @@ public function testRefundFailedNotice() $orderHelper->addRefundFailedNotice($order, $notification); } + public function testUpdatePaymentDetailsWithOrderInitiallyInStatePaymentReview() + { + $pspReference = '123456ABCDEF'; + $notificationMock = $this->createMock(Notification::class); + $notificationMock->method('getPspreference')->willReturn($pspReference); + + $paymentMock = $this->createMock(MagentoOrder\Payment::class); + $paymentMock->expects($this->once())->method('setCcTransId')->with($pspReference); + $paymentMock->expects($this->once())->method('setLastTransId')->with($pspReference); + $paymentMock->expects($this->once())->method('setTransactionId')->with($pspReference); + + $orderMock = $this->createConfiguredMock(MagentoOrder::class, [ + 'getPayment' => $paymentMock, + 'getState' => \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW, + 'setState' => \Magento\Sales\Model\Order::STATE_NEW + ]); + + $transactionMock = $this->createMock(\Magento\Sales\Model\Order\Payment\Transaction::class); + $transactionMock->expects($this->once())->method('setIsClosed')->with(false); + $transactionMock->expects($this->once())->method('save'); + + $transactionBuilderMock = $this->createMock(Builder::class); + $transactionBuilderMock->method('setPayment')->willReturnSelf(); + $transactionBuilderMock->method('setOrder')->willReturnSelf(); + $transactionBuilderMock->method('setTransactionId')->willReturnSelf(); + $transactionBuilderMock->method('build')->willReturn($transactionMock); + + $orderHelper = $this->createOrderHelper( + null, + null, + null, + null, + null, + null, + $transactionBuilderMock + ); + + $result = $orderHelper->updatePaymentDetails($orderMock, $notificationMock); + + $this->assertInstanceOf(\Magento\Sales\Model\Order\Payment\Transaction::class, $result); + } + + public function testUpdatePaymentDetailsWithOrderNotInStatePaymentReview() + { + $pspReference = '123456789'; + $paymentMock = $this->createConfiguredMock(MagentoOrder\Payment::class, [ + 'setCcTransId' => $pspReference, + 'setLastTransId' => $pspReference, + 'setTransactionId' => $pspReference + ]); + $orderMock = $this->createConfiguredMock(MagentoOrder::class, [ + 'getPayment' => $paymentMock, + 'getState' => \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW, + 'setState' => \Magento\Sales\Model\Order::STATE_NEW + ]); + + $notificationMock = $this->createConfiguredMock(Notification::class, [ + 'getPspReference' => $pspReference + ]); + + $transactionBuilderMock = $this->createMock(Builder::class); + $transactionMock = $this->createMock(\Magento\Sales\Model\Order\Payment\Transaction::class); + $transactionBuilderMock->expects($this->once()) + ->method('setPayment') + ->with($paymentMock) + ->willReturnSelf(); + + $transactionBuilderMock->expects($this->once()) + ->method('setOrder') + ->with($orderMock) + ->willReturnSelf(); + + $transactionBuilderMock->expects($this->once()) + ->method('setTransactionId') + ->with($pspReference) + ->willReturnSelf(); + + $transactionBuilderMock->expects($this->once()) + ->method('build') + ->with(TransactionInterface::TYPE_AUTH) + ->willReturn($transactionMock); + + $transactionMock->expects($this->once()) + ->method('setIsClosed') + ->with(false); + + $transactionMock->expects($this->once()) + ->method('save'); + + $orderHelper = $this->createOrderHelper( + null, + null, + null, + null, + null, + null, + $transactionBuilderMock + ); + + $result = $orderHelper->updatePaymentDetails($orderMock, $notificationMock); + + $this->assertEquals($transactionMock, $result); + } + + public function testAddWebhookStatusHistoryComment() + { + $eventCode = 'AUTHORISATION'; + $amountCurrency = 'EUR'; + $amountValue = 1000; + $expectedComment = 'AUTHORISATION webhook notification w/amount EUR 10.00 was processed'; + $dataHelperMock = $this->createMock(Data::class); + $orderMock = $this->createMock(MagentoOrder::class); + $notificationMock = $this->createMock(Notification::class); + + $notificationMock->method('getEventCode')->willReturn($eventCode); + $notificationMock->method('getAmountCurrency')->willReturn($amountCurrency); + $notificationMock->method('getAmountValue')->willReturn($amountValue); + + $dataHelperMock->method('originalAmount')->with($amountValue, $amountCurrency)->willReturn('10.00'); + + $orderMock->expects($this->once()) + ->method('addStatusHistoryComment') + ->with($this->equalTo($expectedComment), $this->equalTo(false)) + ->willReturnSelf(); + + $orderHelper = $this->createOrderHelper( + null, + null, + null, + null, + $dataHelperMock + ); + + $result = $orderHelper->addWebhookStatusHistoryComment($orderMock, $notificationMock); + + $this->assertEquals($orderMock, $result); + } + + public function testSendOrderMailSuccess() + { + $orderMock = $this->createMock(MagentoOrder::class); + $adyenLoggerMock = $this->createMock(AdyenLogger::class); + $orderSenderMock = $this->createMock(OrderSender::class); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Payment::class) + ->disableOriginalConstructor() + ->getMock(); + + $paymentMock->expects($this->exactly(2)) + ->method('getData') + ->willReturnMap([ + ['adyen_psp_reference', null, 'test_psp_reference'], + ['entity_id', null, 'test_entity_id'] + ]); + + $orderMock->expects($this->exactly(2)) + ->method('getPayment') + ->willReturn($paymentMock); + + $orderSenderMock->expects($this->once()) + ->method('send') + ->with($orderMock); + + $adyenLoggerMock->expects($this->once()) + ->method('addAdyenNotification') + ->with( + 'Send order confirmation email to shopper', + ['pspReference' => 'test_psp_reference', 'merchantReference' => 'test_entity_id'] + ); + + $orderHelper = $this->createOrderHelper( + null, + null, + null, + null, + null, + null, + null, + $adyenLoggerMock, + $orderSenderMock + ); + + $orderHelper->sendOrderMail($orderMock); + } + + public function testCreateShipmentSuccess() + { + $adyenLoggerMock = $this->createMock(AdyenLogger::class); + $orderMock = $this->createMock(MagentoOrder::class); + $shipmentMock = $this->createPartialMock(\Magento\Sales\Model\Order\Shipment::class, ['register', 'getOrder', 'addComment']); + $shipmentMock->method('getOrder')->willReturn($orderMock); + $transactionBuilderMock = $this->createMock(Builder::class); + + $orderMock->method('canShip')->willReturn(true); + + $orderHelper = $this->createOrderHelper( + null, + null, + null, + null, + null, + null, + $transactionBuilderMock, + $adyenLoggerMock + ); + + $result = $orderHelper->createShipment($orderMock); + + $this->assertEquals($orderMock, $result); + } + + + public function testCreateShipmentCannotShip() + { + $adyenLoggerMock = $this->createMock(AdyenLogger::class); + $orderMock = $this->createMock(MagentoOrder::class); + $transactionBuilderMock = $this->createMock(Builder::class); + $paymentMock = $this->createMock(\Magento\Sales\Model\Order\Payment::class); + $paymentMock->method('getData') + ->willReturnMap([ + ['adyen_psp_reference', null, 'test_psp_reference'], + ['entity_id', null, 'test_entity_id'] + ]); + $orderMock->method('canShip')->willReturn(false); + $orderMock->method('getPayment')->willReturn($paymentMock); + + $adyenLoggerMock->expects($this->once()) + ->method('addAdyenNotification') + ->with( + 'Order can\'t be shipped', + $this->anything() + ); + + $orderHelper = $this->createOrderHelper( + null, + null, + null, + null, + null, + null, + $transactionBuilderMock, + $adyenLoggerMock + ); + + $result = $orderHelper->createShipment($orderMock); + + $this->assertEquals($orderMock, $result); + } + + public function testSetPrePaymentAuthorized() + { + $storeId = 1; + $status = 'pre_authorized'; + $adyenLoggerMock = $this->createMock(AdyenLogger::class); + + $orderMock = $this->createMock(MagentoOrder::class); + $orderMock->method('getStoreId')->willReturn($storeId); + $orderMock->expects($this->once())->method('setStatus')->with($status); + $orderMock->expects($this->once())->method('getState')->willReturn('new'); + + $configHelperMock = $this->createConfiguredMock(Config::class, ['getConfigData' => $status]); + $adyenLoggerMock->expects($this->atLeastOnce())->method('addAdyenNotification') + ->withConsecutive( + [$this->stringContains('No new state assigned, status should be connected to one of the following states: ["new","adyen_authorized"]')], + [$this->stringContains('Order status is changed to Pre-authorised status')] + ); + + $paymentMock = $this->createMock(\Magento\Sales\Model\Order\Payment::class); + $paymentMock->method('getData')->willReturnMap([ + ['adyen_psp_reference', null, 'test_psp_reference'], + ['entity_id', null, 'test_entity_id'] + ]); + $orderMock->method('getPayment')->willReturn($paymentMock); + $orderStatusCollectionMock = $this->createOrderStatusCollection(MagentoOrder::STATE_PROCESSING); + + $orderHelper = $this->createOrderHelper( + $orderStatusCollectionMock, + $configHelperMock, + null, + null, + null, + null, + null, + $adyenLoggerMock + ); + + $result = $orderHelper->setPrePaymentAuthorized($orderMock); + + $this->assertInstanceOf(MagentoOrder::class, $result); + $this->assertEquals('new', $result->getState()); + } + + public function testSetPrePaymentAuthorizedNoStatus() + { + $storeId = 1; + $eventLabel = "payment_pre_authorized"; + $adyenLoggerMock = $this->createMock(AdyenLogger::class); + + $orderMock = $this->createMock(MagentoOrder::class); + $orderMock->method('getStoreId')->willReturn($storeId); + $orderMock->method('getState')->willReturn('new'); + + $configHelperMock = $this->createMock(Config::class); + $configHelperMock->method('getConfigData') + ->with($eventLabel, 'adyen_abstract', $storeId) + ->willReturn(''); + + $adyenLoggerMock->expects($this->once())->method('addAdyenNotification') + ->with( + $this->stringContains('No pre-authorised status is used so ignore'), + $this->arrayHasKey('pspReference') + ); + + $paymentMock = $this->createMock(\Magento\Sales\Model\Order\Payment::class); + $paymentMock->method('getData')->willReturnMap([ + ['adyen_psp_reference', null, 'test_psp_reference'], + ['entity_id', null, 'test_entity_id'] + ]); + $orderMock->method('getPayment')->willReturn($paymentMock); + + $orderHelper = $this->createOrderHelper( + null, + $configHelperMock, + null, + null, + null, + null, + null, + $adyenLoggerMock + ); + + $result = $orderHelper->setPrePaymentAuthorized($orderMock); + + $this->assertInstanceOf(MagentoOrder::class, $result); + $this->assertEquals('new', $result->getState()); + } + + public function testSetStatusOrderCreation() + { + $paymentMethodCode = 'adyen_cc'; + $storeId = 1; + $assignedStatusForStateNew = 'pending'; + + $paymentMock = $this->createMock(MagentoOrder\Payment::class); + $paymentMock->method('getMethod')->willReturn($paymentMethodCode); + + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->method('getPayment')->willReturn($paymentMock); + $orderMock->method('getStoreId')->willReturn($storeId); + + $configHelper = $this->createMock(Config::class); + $configHelper->method('getConfigData')->with('order_status', $paymentMethodCode, $storeId) + ->willReturn(\Magento\Sales\Model\Order::STATE_NEW); + + $statusResolverMock = $this->createMock(MagentoOrder\StatusResolver::class); + $statusResolverMock->method('getOrderStatusByState')->willReturn($assignedStatusForStateNew); + + $dataHelper = $this->createOrderHelper( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + $statusResolverMock + ); + + $result = $dataHelper->setStatusOrderCreation($orderMock); + + $this->assertInstanceOf(OrderInterface::class, $result); + } + protected function createOrderHelper( $orderStatusCollectionFactory = null, $configHelper = null, @@ -273,7 +700,8 @@ protected function createOrderHelper( $notifierPool = null, $paymentMethodsHelper = null, $adyenCreditmemoResourceModel = null, - $adyenCreditmemoHelper = null + $adyenCreditmemoHelper = null, + $statusResolver = null ): Order { $context = $this->createMock(Context::class); @@ -342,6 +770,10 @@ protected function createOrderHelper( $adyenCreditmemoHelper = $this->createMock(AdyenCreditmemoHelper::class); } + if (is_null($statusResolver)) { + $statusResolver = $this->createMock(MagentoOrder\StatusResolver::class); + } + return new Order( $context, $builder, @@ -359,7 +791,8 @@ protected function createOrderHelper( $orderPaymentCollectionFactory, $paymentMethodsHelper, $adyenCreditmemoResourceModel, - $adyenCreditmemoHelper + $adyenCreditmemoHelper, + $statusResolver ); } } diff --git a/Test/Unit/Helper/PaymentDetailsTest.php b/Test/Unit/Helper/PaymentDetailsTest.php index eef777514..7406661b4 100644 --- a/Test/Unit/Helper/PaymentDetailsTest.php +++ b/Test/Unit/Helper/PaymentDetailsTest.php @@ -10,13 +10,14 @@ */ namespace Adyen\Payment\Test\Unit\Helper; +use Adyen\AdyenException; use Adyen\Payment\Helper\PaymentsDetails; use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Framework\Exception\ValidatorException; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order\Payment; use Adyen\Payment\Helper\Data; use Adyen\Payment\Logger\AdyenLogger; -use Adyen\Payment\Helper\PaymentResponseHandler; use Adyen\Payment\Helper\Idempotency; use Magento\Checkout\Model\Session; use Adyen\Service\Checkout; @@ -27,72 +28,95 @@ class PaymentDetailsTest extends AbstractAdyenTestCase private $checkoutSessionMock; private $adyenHelperMock; private $adyenLoggerMock; - private $paymentResponseHandlerMock; private $idempotencyHelperMock; private $paymentDetails; + private $orderMock; + private $paymentMock; + private $checkoutServiceMock; + private $adyenClientMock; + protected function setUp(): void { $this->checkoutSessionMock = $this->createMock(Session::class); $this->adyenHelperMock = $this->createMock(Data::class); $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); - $this->paymentResponseHandlerMock = $this->createMock(PaymentResponseHandler::class); $this->idempotencyHelperMock = $this->createMock(Idempotency::class); + $this->orderMock = $this->createMock(OrderInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->checkoutServiceMock = $this->createMock(Checkout::class); + $this->adyenClientMock = $this->createMock(Client::class); + + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->orderMock->method('getStoreId')->willReturn(1); + $this->paymentMock->method('getOrder')->willReturn($this->orderMock); + + $this->adyenHelperMock->method('initializeAdyenClient')->willReturn($this->adyenClientMock); + $this->adyenHelperMock->method('createAdyenCheckoutService')->willReturn($this->checkoutServiceMock); + $this->paymentDetails = new PaymentsDetails( $this->checkoutSessionMock, $this->adyenHelperMock, $this->adyenLoggerMock, - $this->paymentResponseHandlerMock, $this->idempotencyHelperMock - ); + ); } - public function testRequestHeadersAreAddedToRequest() + public function testInitiatePaymentDetailsSuccessfully() { - $orderMock = $this->createMock(OrderInterface::class); - $paymentMock = $this->createMock(Payment::class); - $checkoutServiceMock = $this->createMock(Checkout::class); - $adyenClientMock = $this->createMock(Client::class); - $storeId = 1; - $payload = json_encode([ - 'details' => 'some_details', + $payload = [ + 'details' => [ + 'detail_key1' => 'some-details', + 'merchantReference' => '00000000001' + ], 'paymentData' => 'some_payment_data', - 'threeDSAuthenticationOnly' => true - ]); + 'threeDSAuthenticationOnly' => true, + ]; + $requestOptions = [ - 'idempotencyKey' => 'some_idempotency_key', - 'headers' => ['headerKey' => 'headerValue'] + 'idempotencyKey' => 'some_idempotency_key', + 'headers' => ['headerKey' => 'headerValue'] ]; - $paymentDetailsResult = ['resultCode' => 'Authorised', 'action' => null, 'additionalData' => null]; - $orderMock->method('getPayment')->willReturn($paymentMock); - $orderMock->method('getStoreId')->willReturn($storeId); - $paymentMock->method('getOrder')->willReturn($orderMock); + $paymentDetailsResult = ['resultCode' => 'Authorised', 'action' => null, 'additionalData' => null]; - $this->adyenHelperMock->method('initializeAdyenClient')->willReturn($adyenClientMock); - $this->adyenHelperMock->method('createAdyenCheckoutService')->willReturn($checkoutServiceMock); $this->adyenHelperMock->method('buildRequestHeaders')->willReturn($requestOptions['headers']); $this->idempotencyHelperMock->method('generateIdempotencyKey')->willReturn($requestOptions['idempotencyKey']); - $checkoutServiceMock->expects($this->once()) + // testing cleanUpPaymentDetailsPayload() method + $apiPayload = $payload; + unset($apiPayload['details']['merchantReference']); + + $this->checkoutServiceMock->expects($this->once()) ->method('paymentsDetails') - ->with( - $this->equalTo([ - 'details' => 'some_details', - 'paymentData' => 'some_payment_data', - 'threeDSAuthenticationOnly' => true - ]), - $this->equalTo($requestOptions) - ) + ->with($apiPayload, $requestOptions) ->willReturn($paymentDetailsResult); - $this->paymentResponseHandlerMock->method('handlePaymentResponse')->willReturn(true); - $this->paymentResponseHandlerMock->method('formatPaymentResponse')->willReturn($paymentDetailsResult); + $result = $this->paymentDetails->initiatePaymentDetails($this->orderMock, $payload); + + $this->assertIsArray($result); + $this->assertEquals($paymentDetailsResult, $result); + } + + public function testInitiatePaymentDetailsFailure() + { + $this->expectException(ValidatorException::class); + + $payload = [ + 'details' => [ + 'detail_key1' => 'some-details', + 'merchantReference' => '00000000001' + ], + 'paymentData' => 'some_payment_data', + 'threeDSAuthenticationOnly' => true, + ]; + + $this->checkoutServiceMock->method('paymentsDetails')->willThrowException(new AdyenException()); - $result = $this->paymentDetails->initiatePaymentDetails($orderMock, $payload); + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('error'); + $this->checkoutSessionMock->expects($this->atLeastOnce())->method('restoreQuote'); - $this->assertJson($result); - $this->assertEquals(json_encode($paymentDetailsResult), $result); + $this->paymentDetails->initiatePaymentDetails($this->orderMock, $payload); } } diff --git a/Test/Unit/Helper/PaymentResponseHandlerTest.php b/Test/Unit/Helper/PaymentResponseHandlerTest.php new file mode 100644 index 000000000..8a5b0fc76 --- /dev/null +++ b/Test/Unit/Helper/PaymentResponseHandlerTest.php @@ -0,0 +1,434 @@ + + */ +namespace Adyen\Payment\Test\Unit\Helper; + +namespace Adyen\Payment\Test\Unit\Helper; + +use Adyen\Payment\Helper\PaymentResponseHandler; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Helper\Vault; +use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\Quote; +use Adyen\Payment\Helper\Order as OrderHelper; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Exception; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Status\History; +use Magento\Sales\Model\ResourceModel\Order; +use Magento\Sales\Model\OrderRepository; +use Magento\Sales\Model\Order\Status\HistoryFactory; +use Adyen\Payment\Helper\StateData; + +class PaymentResponseHandlerTest extends AbstractAdyenTestCase +{ + private $paymentMock; + private $orderMock; + private $adyenLoggerMock; + private $vaultHelperMock; + private $orderResourceModelMock; + private $dataHelperMock; + private $quoteHelperMock; + private $orderHelperMock; + private $orderRepositoryMock; + private $orderHistoryFactoryMock; + private $stateDataHelperMock; + + private $paymentResponseHandler; + + protected function setUp(): void + { + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(\Magento\Sales\Model\Order::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + $this->vaultHelperMock = $this->createMock(Vault::class); + $this->orderResourceModelMock = $this->createMock(Order::class); + $this->dataHelperMock = $this->createMock(Data::class); + $this->quoteHelperMock = $this->createMock(Quote::class); + $this->orderHelperMock = $this->createMock(OrderHelper::class); + $this->orderRepositoryMock = $this->createMock(OrderRepository::class); + $this->orderHistoryFactoryMock = $this->createGeneratedMock(HistoryFactory::class, [ + 'create' + ]); + $this->stateDataHelperMock = $this->createMock(StateData::class); + + $orderHistory = $this->createMock(History::class); + $orderHistory->method('setStatus')->willReturnSelf(); + $orderHistory->method('setComment')->willReturnSelf(); + $orderHistory->method('setEntityName')->willReturnSelf(); + $orderHistory->method('setOrder')->willReturnSelf(); + + $this->orderHistoryFactoryMock->method('create')->willReturn($orderHistory); + $this->orderMock->method('getQuoteId')->willReturn(1); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->orderMock->method('getStatus')->willReturn('pending'); + + $this->orderHelperMock->method('setStatusOrderCreation')->willReturn( $this->orderMock); + + $this->paymentResponseHandler = new PaymentResponseHandler( + $this->adyenLoggerMock, + $this->vaultHelperMock, + $this->orderResourceModelMock, + $this->dataHelperMock, + $this->quoteHelperMock, + $this->orderHelperMock, + $this->orderRepositoryMock, + $this->orderHistoryFactoryMock, + $this->stateDataHelperMock + ); + } + + private static function dataSourceForFormatPaymentResponseFinalResultCodes(): array + { + return [ + ['resultCode' => PaymentResponseHandler::AUTHORISED], + ['resultCode' => PaymentResponseHandler::REFUSED], + ['resultCode' => PaymentResponseHandler::ERROR], + ['resultCode' => PaymentResponseHandler::POS_SUCCESS] + ]; + } + + /** + * @param $resultCode + * @return void + * @dataProvider dataSourceForFormatPaymentResponseFinalResultCodes + */ + public function testFormatPaymentResponseForFinalResultCodes($resultCode) + { + $expectedResult = [ + "isFinal" => true, + "resultCode" => $resultCode + ]; + + // Execute method of the tested class + $result = $this->paymentResponseHandler->formatPaymentResponse($resultCode); + + // Assert conditions + $this->assertEquals($expectedResult, $result); + } + + private static function dataSourceForFormatPaymentResponseActionRequredPayments(): array + { + return [ + ['resultCode' => PaymentResponseHandler::REDIRECT_SHOPPER, 'action' => ['type' => 'qrCode']], + ['resultCode' => PaymentResponseHandler::IDENTIFY_SHOPPER, 'action' => ['type' => 'qrCode']], + ['resultCode' => PaymentResponseHandler::CHALLENGE_SHOPPER, 'action' => ['type' => 'qrCode']], + ['resultCode' => PaymentResponseHandler::PENDING, 'action' => ['type' => 'qrCode']], + ]; + } + + /** + * @param $resultCode + * @param $action + * @return void + * @dataProvider dataSourceForFormatPaymentResponseActionRequredPayments + */ + public function testFormatPaymentResponseForActionRequiredPayments($resultCode, $action) + { + $expectedResult = [ + "isFinal" => false, + "resultCode" => $resultCode, + "action" => $action + ]; + + // Execute method of the tested class + $result = $this->paymentResponseHandler->formatPaymentResponse($resultCode, $action); + + // Assert conditions + $this->assertEquals($expectedResult, $result); + } + + /** + * @return void + */ + public function testFormatPaymentResponseForVoucherPayments() + { + $resultCode = PaymentResponseHandler::PRESENT_TO_SHOPPER; + $action = ['type' => 'voucher']; + + $expectedResult = [ + "isFinal" => true, + "resultCode" => $resultCode, + "action" => $action + ]; + + // Execute method of the tested class + $result = $this->paymentResponseHandler->formatPaymentResponse($resultCode, $action); + + // Assert conditions + $this->assertEquals($expectedResult, $result); + } + + /** + * @return void + */ + public function testFormatPaymentResponseForOfflinePayments() + { + $resultCode = PaymentResponseHandler::RECEIVED; + $additionalData = ['action' => ['voucher']]; + + $expectedResult = [ + "isFinal" => true, + "resultCode" => $resultCode, + "additionalData" => $additionalData + ]; + + // Execute method of the tested class + $result = $this->paymentResponseHandler->formatPaymentResponse($resultCode, null, $additionalData); + + // Assert conditions + $this->assertEquals($expectedResult, $result); + } + + /** + * @return void + */ + public function testFormatPaymentResponseForUnknownResults() + { + $resultCode = 'UNRECOGNISED_RESULT_CODE'; + + $expectedResult = [ + "isFinal" => true, + "resultCode" => PaymentResponseHandler::ERROR + ]; + + // Execute method of the tested class + $result = $this->paymentResponseHandler->formatPaymentResponse($resultCode); + + // Assert conditions + $this->assertEquals($expectedResult, $result); + } + + public function testHandlePaymentsDetailsResponseWithNullResultCode() + { + $orderMock = $this->createMock(\Magento\Sales\Model\Order::class); + + $paymentsDetailsResponse = [ + 'randomData' => 'someRandomValue' + ]; + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $orderMock + ); + + $this->assertFalse($result); + } + + public function testHandlePaymentsDetailsResponseAuthorised() + { + $paymentsDetailsResponse = [ + 'resultCode' => PaymentResponseHandler::AUTHORISED, + 'pspReference' => 'ABC123456789', + 'paymentMethod' => [ + 'brand' => 'ideal' + ], + 'additionalData' => [ + 'someData' => 'someValue' + ], + 'details' => [ + 'someData' => 'someValue' + ], + 'donationToken' => 'XYZ123456789' + ]; + + $this->quoteHelperMock->method('disableQuote')->willThrowException(new Exception()); + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('error'); + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + + $this->assertTrue($result); + } + + + private static function handlePaymentsDetailsPendingProvider(): array + { + return [ + ['paymentMethodCode' => 'bankTransfer'], + ['paymentMethodCode' => 'sepadirectdebit'], + ['paymentMethodCode' => 'multibanco'], + ]; + } + + /** + * @return void + * @throws AlreadyExistsException + * @throws InputException + * @throws NoSuchEntityException + * @dataProvider handlePaymentsDetailsPendingProvider + */ + public function testHandlePaymentsDetailsResponsePending($paymentMethodCode) + { + $this->stateDataHelperMock->method('cleanQuoteStateData') + ->willThrowException(new Exception); + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('error'); + + $paymentsDetailsResponse = [ + 'resultCode' => PaymentResponseHandler::PENDING, + 'pspReference' => 'ABC123456789', + 'paymentMethod' => [ + 'brand' => $paymentMethodCode + ] + ]; + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + + $this->assertTrue($result); + } + + private static function handlePaymentsDetailsPendingReceived(): array + { + return [ + ['paymentMethodCode' => 'alipay_hk', 'expectedResult' => false], + ['paymentMethodCode' => 'multibanco', 'expectedResult' => true] + ]; + } + + /** + * @return void + * @throws AlreadyExistsException + * @throws InputException + * @throws NoSuchEntityException + * @dataProvider handlePaymentsDetailsPendingReceived + */ + public function testHandlePaymentsDetailsResponseReceived($paymentMethodCode, $expectedResult) + { + $paymentsDetailsResponse = [ + 'resultCode' => PaymentResponseHandler::RECEIVED, + 'pspReference' => 'ABC123456789', + 'paymentMethod' => [ + 'brand' => $paymentMethodCode + ] + ]; + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + + $this->assertEquals($expectedResult, $result); + } + + private static function handlePaymentsDetailsActionRequiredProvider(): array + { + return [ + ['resultCode' => PaymentResponseHandler::PRESENT_TO_SHOPPER], + ['resultCode' => PaymentResponseHandler::IDENTIFY_SHOPPER], + ['resultCode' => PaymentResponseHandler::CHALLENGE_SHOPPER], + ['resultCode' => PaymentResponseHandler::REDIRECT_SHOPPER] + ]; + } + + /** + * @return void + * @throws AlreadyExistsException + * @throws InputException + * @throws NoSuchEntityException + * @dataProvider handlePaymentsDetailsActionRequiredProvider + */ + public function testHandlePaymentsDetailsResponseActionRequired($resultCode) + { + $paymentsDetailsResponse = [ + 'resultCode' => $resultCode, + 'pspReference' => 'ABC123456789', + 'paymentMethod' => [ + 'brand' => 'ideal' + ], + 'action' => [ + 'actionData' => 'actionValue' + ] + ]; + + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('addAdyenResult'); + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + + $this->assertTrue($result); + } + + private static function handlePaymentsDetailsActionCancelledOrRefusedProvider(): array + { + return [ + ['resultCode' => PaymentResponseHandler::REFUSED], + ['resultCode' => PaymentResponseHandler::CANCELLED] + ]; + } + + /** + * @return void + * @throws AlreadyExistsException + * @throws InputException + * @throws NoSuchEntityException + * @dataProvider handlePaymentsDetailsActionCancelledOrRefusedProvider + */ + public function testHandlePaymentsDetailsResponseCancelOrRefused($resultCode) + { + $paymentsDetailsResponse = [ + 'resultCode' => $resultCode, + 'pspReference' => 'ABC123456789', + 'paymentMethod' => [ + 'brand' => 'ideal' + ], + 'action' => [ + 'actionData' => 'actionValue' + ] + ]; + + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('addAdyenResult'); + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + + $this->assertFalse($result); + } + + public function testHandlePaymentsDetailsResponseInvalid() + { + $paymentsDetailsResponse = [ + 'resultCode' => 'UNRECOGNISED_RESULT_CODE' + ]; + + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('error'); + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + + $this->assertFalse($result); + } + + public function testHandlePaymentsDetailsEmptyResponse() + { + $paymentsDetailsResponse = []; + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('error'); + + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + + $this->assertFalse($result); + } +} diff --git a/Test/Unit/Model/Api/AdyenPaymentsDetailsTest.php b/Test/Unit/Model/Api/AdyenPaymentsDetailsTest.php new file mode 100644 index 000000000..65b195b9c --- /dev/null +++ b/Test/Unit/Model/Api/AdyenPaymentsDetailsTest.php @@ -0,0 +1,106 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Model\Api; + +use Adyen\Payment\Helper\PaymentResponseHandler; +use Adyen\Payment\Helper\PaymentsDetails; +use Adyen\Payment\Model\Api\AdyenPaymentsDetails; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; + +class AdyenPaymentsDetailsTest extends AbstractAdyenTestCase +{ + private $adyenPaymentsDetails; + private $orderRepositoryMock; + private $paymentsDetailsHelperMock; + private $paymentResponseHandlerHelperMock; + + protected function setUp(): void + { + $this->orderRepositoryMock = $this->createMock(OrderRepositoryInterface::class); + $this->paymentsDetailsHelperMock = $this->createMock(PaymentsDetails::class); + $this->paymentResponseHandlerHelperMock = $this->createPartialMock( + PaymentResponseHandler::class, + ['handlePaymentsDetailsResponse'] + ); + + $objectManager = new ObjectManager($this); + $this->adyenPaymentsDetails = $objectManager->getObject(AdyenPaymentsDetails::class, [ + 'orderRepository' => $this->orderRepositoryMock, + 'paymentsDetails' => $this->paymentsDetailsHelperMock, + 'paymentResponseHandler' => $this->paymentResponseHandlerHelperMock + ]); + } + + public function testSuccessfulCall() + { + $payload = '{"someData":"someValue"}'; + $result = ['resultCode' => 'Authorised']; + $orderId = 1; + + $this->orderRepositoryMock + ->method('get') + ->willReturn($this->createMock(OrderInterface::class)); + + $this->paymentsDetailsHelperMock + ->method('initiatePaymentDetails') + ->willReturn($result); + + $this->paymentResponseHandlerHelperMock + ->method('handlePaymentsDetailsResponse') + ->willReturn(true); + + $response = $this->adyenPaymentsDetails->initiate($payload, $orderId); + + $this->assertJson($response); + $this->assertArrayHasKey('isFinal', json_decode($response, true)); + $this->assertArrayHasKey('resultCode', json_decode($response, true)); + } + + public function testFailingJson() + { + $this->expectException(ValidatorException::class); + + $payload = '{"someData":"someValue"'; + $orderId = 1; + + $this->adyenPaymentsDetails->initiate($payload, $orderId); + } + + public function testInvalidDetailsCall() + { + $this->expectException(ValidatorException::class); + + $payload = '{"someData":"someValue"}'; + $result = ['resultCode' => 'Authorised']; + $orderId = 1; + + $this->orderRepositoryMock + ->method('get') + ->willReturn($this->createMock(OrderInterface::class)); + + $this->paymentsDetailsHelperMock + ->method('initiatePaymentDetails') + ->willReturn($result); + + $this->paymentResponseHandlerHelperMock + ->method('handlePaymentsDetailsResponse') + ->willReturn(false); + + $this->adyenPaymentsDetails->initiate($payload, $orderId); + } + + +} diff --git a/Test/Unit/Model/Api/GuestAdyenPaymentsDetailsTest.php b/Test/Unit/Model/Api/GuestAdyenPaymentsDetailsTest.php new file mode 100644 index 000000000..76d7873d8 --- /dev/null +++ b/Test/Unit/Model/Api/GuestAdyenPaymentsDetailsTest.php @@ -0,0 +1,110 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Model\Api; + +use Adyen\Payment\Model\Api\AdyenPaymentsDetails; +use Adyen\Payment\Model\Api\GuestAdyenPaymentsDetails; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; + +class GuestAdyenPaymentsDetailsTest extends AbstractAdyenTestCase +{ + private $guestAdyenPaymentsDetails; + private $orderRepositoryMock; + private $quoteIdMaskFactoryMask; + private $adyenPaymentsDetailsMock; + + protected function setUp(): void + { + $this->orderRepositoryMock = $this->createMock(OrderRepositoryInterface::class); + $this->adyenPaymentsDetailsMock = $this->createMock(AdyenPaymentsDetails::class); + $this->quoteIdMaskFactoryMask = $this->createGeneratedMock(QuoteIdMaskFactory::class, [ + 'create' + ]); + + $objectManager = new ObjectManager($this); + $this->guestAdyenPaymentsDetails = $objectManager->getObject(GuestAdyenPaymentsDetails::class, [ + 'orderRepository' => $this->orderRepositoryMock, + 'adyenPaymentsDetails' => $this->adyenPaymentsDetailsMock, + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMask + ]); + } + + public function testSuccessfulCall() + { + $payload = '{"someData":"someValue"}'; + $result = '{"resultCode": "Authorised", "isFinal": "true"}'; + $orderId = 1; + $maskedCartId = 'abcdef123456'; + $cartId = 99; + $orderQuoteId = 99; + + $quoteIdMaskMock = $this->createGeneratedMock(QuoteIdMask::class, [ + 'load', + 'getQuoteId' + ]); + $quoteIdMaskMock->method('load')->willReturn($quoteIdMaskMock); + $quoteIdMaskMock->method('getQuoteId')->willReturn($cartId); + + $this->quoteIdMaskFactoryMask->method('create') + ->willReturn($quoteIdMaskMock); + + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->method('getQuoteId')->willReturn($orderQuoteId); + + $this->orderRepositoryMock->method('get') + ->willReturn($orderMock); + + $this->adyenPaymentsDetailsMock->method('initiate') + ->willReturn($result); + + $response = $this->guestAdyenPaymentsDetails->initiate($payload, $orderId, $maskedCartId); + + $this->assertJson($response); + $this->assertArrayHasKey('isFinal', json_decode($response, true)); + $this->assertArrayHasKey('resultCode', json_decode($response, true)); + } + + public function testWrongCartId() + { + $this->expectException(NotFoundException::class); + + $payload = '{"someData":"someValue"}'; + $orderId = 1; + $maskedCartId = 'abcdef123456'; + $cartId = 99; + $orderQuoteId = 200; + + $quoteIdMaskMock = $this->createGeneratedMock(QuoteIdMask::class, [ + 'load', + 'getQuoteId' + ]); + $quoteIdMaskMock->method('load')->willReturn($quoteIdMaskMock); + $quoteIdMaskMock->method('getQuoteId')->willReturn($cartId); + + $this->quoteIdMaskFactoryMask->method('create') + ->willReturn($quoteIdMaskMock); + + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->method('getQuoteId')->willReturn($orderQuoteId); + + $this->orderRepositoryMock->method('get') + ->willReturn($orderMock); + + $this->guestAdyenPaymentsDetails->initiate($payload, $orderId, $maskedCartId); + } +} diff --git a/Test/Unit/Model/Api/TokenDeactivateTest.php b/Test/Unit/Model/Api/TokenDeactivateTest.php new file mode 100644 index 000000000..0a56f9852 --- /dev/null +++ b/Test/Unit/Model/Api/TokenDeactivateTest.php @@ -0,0 +1,128 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Model\Api; + +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Adyen\Payment\Model\Api\TokenDeactivate; +use Adyen\Payment\Logger\AdyenLogger; +use Magento\Vault\Api\PaymentTokenRepositoryInterface; +use Magento\Vault\Model\PaymentTokenManagement; + +class TokenDeactivateTest extends AbstractAdyenTestCase +{ + private $paymentTokenRepositoryMock; + private $paymentTokenManagementMock; + private $adyenLoggerMock; + private $tokenDeactivate; + + protected function setUp(): void + { + $this->paymentTokenRepositoryMock = $this->createMock(PaymentTokenRepositoryInterface::class); + $this->paymentTokenManagementMock = $this->createMock(PaymentTokenManagement::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + + $this->tokenDeactivate = new TokenDeactivate( + $this->paymentTokenRepositoryMock, + $this->paymentTokenManagementMock, + $this->adyenLoggerMock + ); + } + + public function testSuccessfullyDeactivatePaymentToken() + { + $paymentToken = 'token123'; + $paymentMethodCode = 'adyen_cc'; + $customerId = 1; + $expectedResult = true; + + $paymentTokenMock = $this->createMock(\Magento\Vault\Api\Data\PaymentTokenInterface::class); + $paymentTokenMock->method('getEntityId')->willReturn('123'); + + $this->paymentTokenManagementMock->expects($this->once()) + ->method('getByGatewayToken') + ->with($paymentToken, $paymentMethodCode, $customerId) + ->willReturn($paymentTokenMock); + + $this->paymentTokenRepositoryMock->expects($this->once()) + ->method('delete') + ->with($paymentTokenMock) + ->willReturn($expectedResult); + + $result = $this->tokenDeactivate->deactivateToken($paymentToken, $paymentMethodCode, $customerId); + + $this->assertEquals($expectedResult, $result); + } + + public function testAttemptToDeactivateNonExistentPaymentToken() + { + $paymentToken = null; + $paymentMethodCode = 'adyen_cc'; + $customerId = 1; + + $this->paymentTokenManagementMock->expects($this->once()) + ->method('getByGatewayToken') + ->with($this->equalTo('non_existent_token'), $this->equalTo($paymentMethodCode), $this->equalTo($customerId)) + ->willReturn($paymentToken); + + $result = $this->tokenDeactivate->deactivateToken('non_existent_token', $paymentMethodCode, $customerId); + + $this->assertFalse($result, "Expected the result to be false when attempting to deactivate a non-existent payment token."); + } + + public function testDeactivateTokenWithInvalidCustomerId() + { + $paymentToken = 'fakeToken'; + $paymentMethodCode = 'adyen_cc'; + $customerId = 999; + + $this->paymentTokenManagementMock->expects($this->once()) + ->method('getByGatewayToken') + ->with($paymentToken, $paymentMethodCode, $customerId) + ->willReturn(null); + + $this->adyenLoggerMock->expects($this->never()) + ->method('error'); + + $result = $this->tokenDeactivate->deactivateToken($paymentToken, $paymentMethodCode, $customerId); + + $this->assertFalse($result, "Expected the result to be false when deactivating a token with an invalid customer ID."); + } + + public function testExceptionThrownDuringTokenDeletion() + { + $paymentToken = 'token123'; + $paymentMethodCode = 'adyen_cc'; + $customerId = 1; + $exceptionMessage = 'Error during deletion'; + + $paymentTokenMock = $this->createMock(\Magento\Vault\Api\Data\PaymentTokenInterface::class); + $paymentTokenMock->method('getEntityId')->willReturn('123'); + + $this->paymentTokenManagementMock->expects($this->once()) + ->method('getByGatewayToken') + ->with($paymentToken, $paymentMethodCode, $customerId) + ->willReturn($paymentTokenMock); + + $this->paymentTokenRepositoryMock->expects($this->once()) + ->method('delete') + ->with($paymentTokenMock) + ->willThrowException(new \Exception($exceptionMessage)); + + $this->adyenLoggerMock->expects($this->once()) + ->method('error') + ->with($this->stringContains('Error while attempting to deactivate token with id 123')); + + $result = $this->tokenDeactivate->deactivateToken($paymentToken, $paymentMethodCode, $customerId); + + $this->assertFalse($result); + } +} diff --git a/Test/Unit/Observer/SetOrderStateAfterPaymentObserverTest.php b/Test/Unit/Observer/SetOrderStateAfterPaymentObserverTest.php new file mode 100644 index 000000000..785f64804 --- /dev/null +++ b/Test/Unit/Observer/SetOrderStateAfterPaymentObserverTest.php @@ -0,0 +1,102 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Observer; + +use Adyen\Payment\Helper\PaymentResponseHandler; +use Adyen\Payment\Model\Method\Adapter; +use Adyen\Payment\Observer\SetOrderStateAfterPaymentObserver; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Framework\Event\Observer; +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; + +class SetOrderStateAfterPaymentObserverTest extends AbstractAdyenTestCase +{ + private $setOrderStateAfterPaymentObserver; + private $observerMock; + private $paymentMock; + private $orderMock; + private $statusResolverMock; + + public function setUp(): void + { + $this->observerMock = $this->createMock(Observer::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->statusResolverMock = $this->createMock(Order\StatusResolver::class); + + $paymentMethodInstanceMock = $this->createMock(Adapter::class); + $this->paymentMock->method('getMethodInstance')->willReturn($paymentMethodInstanceMock); + $this->paymentMock->method('getOrder')->willReturn($this->orderMock); + $this->observerMock->method('getData')->with('payment')->willReturn($this->paymentMock); + $this->statusResolverMock->method('getOrderStatusByState') + ->willReturn(Order::STATE_PENDING_PAYMENT); + + $this->setOrderStateAfterPaymentObserver = new SetOrderStateAfterPaymentObserver( + $this->statusResolverMock + ); + } + + private static function resultCodeProvider(): array + { + return [ + [ + 'resultCode' => PaymentResponseHandler::REDIRECT_SHOPPER, + 'action' => ['type' => 'TYPE_PLACEHOLDER'] + ], + [ + 'resultCode' => PaymentResponseHandler::CHALLENGE_SHOPPER, + 'action' => ['type' => 'TYPE_PLACEHOLDER'] + ], + [ + 'resultCode' => PaymentResponseHandler::PENDING, + 'action' => ['type' => 'TYPE_PLACEHOLDER'] + ], + [ + 'resultCode' => PaymentResponseHandler::IDENTIFY_SHOPPER, + 'action' => ['type' => 'TYPE_PLACEHOLDER'] + ], + [ + 'resultCode' => PaymentResponseHandler::AUTHORISED, + 'action' => null, + 'changeStatus' => false + ] + ]; + } + + /** + * @dataProvider resultCodeProvider + * @return void + * @throws LocalizedException + */ + public function testExecute($resultCode, $action, $changeStatus = true) + { + $this->paymentMock->method('getAdditionalInformation')->will( + $this->returnValueMap([ + ['resultCode', $resultCode], + ['action', $action] + ]) + ); + + if ($changeStatus) { + $this->orderMock->expects($this->once())->method('setState'); + $this->orderMock->expects($this->once())->method('save'); + } else { + $this->orderMock->expects($this->never())->method('setState'); + $this->orderMock->expects($this->never())->method('save'); + } + + $this->setOrderStateAfterPaymentObserver->execute($this->observerMock); + + } +} diff --git a/Test/Unit/Observer/SubmitQuoteObserverTest.php b/Test/Unit/Observer/SubmitQuoteObserverTest.php new file mode 100644 index 000000000..f096ba069 --- /dev/null +++ b/Test/Unit/Observer/SubmitQuoteObserverTest.php @@ -0,0 +1,90 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Observer; + +use Adyen\Payment\Helper\PaymentMethods; +use Adyen\Payment\Observer\SubmitQuoteObserver; +use Adyen\Payment\Helper\PaymentResponseHandler; +use Magento\Framework\Event\Observer; +use Magento\Quote\Model\Quote; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\TestCase; + +class SubmitQuoteObserverTest extends TestCase +{ + private $paymentMethodsHelperMock; + private $submitQuoteObserver; + private $observerMock; + private $orderMock; + private $paymentMock; + private $quoteMock; + private $eventMock; + + protected function setUp(): void + { + $this->paymentMethodsHelperMock = $this->createMock(PaymentMethods::class); + $this->observerMock = $this->createMock(Observer::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->eventMock = $this->getMockBuilder(\Magento\Framework\Event::class) + ->addMethods(['getOrder', 'getQuote']) + ->getMock(); + + $this->submitQuoteObserver = new SubmitQuoteObserver( + $this->paymentMethodsHelperMock + ); + } + + public function testObserverPaymentMethodRequiresAction() + { + $this->observerMock->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->method('getOrder')->willReturn($this->orderMock); + $this->eventMock->method('getQuote')->willReturn($this->quoteMock); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->paymentMock->method('getMethod')->willReturn('adyen_method'); + $this->paymentMock->method('getAdditionalInformation')->with('resultCode') + ->willReturn(PaymentResponseHandler::ACTION_REQUIRED_STATUSES[0]); + + $this->paymentMethodsHelperMock->method('isAdyenPayment')->with('adyen_method') + ->willReturn(true); + + $this->quoteMock->expects($this->once())->method('setIsActive')->with(true); + + $this->submitQuoteObserver->execute($this->observerMock); + } + + public function testObserverPaymentMethodNotRequiresAction() + { + // Setup test scenario + $resultCode = 'Authorised'; // Assuming 'Authorised' is not in ACTION_REQUIRED_STATUSES + + $this->observerMock->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->method('getOrder')->willReturn($this->orderMock); + $this->eventMock->method('getQuote')->willReturn($this->quoteMock); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->paymentMock->method('getMethod')->willReturn('adyen_method'); + $this->paymentMock->method('getAdditionalInformation')->with('resultCode') + ->willReturn($resultCode); + + $this->paymentMethodsHelperMock->method('isAdyenPayment')->with('adyen_method') + ->willReturn(true); + + // Execute method of the tested class + $this->submitQuoteObserver->execute($this->observerMock); + + // Assert conditions + // Verify that the quote's setIsActive method is not called, as the cart status should not be altered + $this->quoteMock->expects($this->never())->method('setIsActive'); + } +} diff --git a/Test/Unit/Plugin/PaymentVaultDeleteTokenTest.php b/Test/Unit/Plugin/PaymentVaultDeleteTokenTest.php new file mode 100644 index 000000000..83180d078 --- /dev/null +++ b/Test/Unit/Plugin/PaymentVaultDeleteTokenTest.php @@ -0,0 +1,124 @@ + + */ + +namespace Adyen\Payment\Test\Plugin; + +use Adyen\Payment\Helper\Data; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Plugin\PaymentVaultDeleteToken; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Adyen\Payment\Helper\Requests; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Api\PaymentTokenRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Adyen\Service\Recurring; +use Adyen\AdyenException; +use Adyen\Client; + +class PaymentVaultDeleteTokenTest extends AbstractAdyenTestCase +{ + private $storeManagerMock; + private $dataHelperMock; + private $adyenLoggerMock; + private $requestsHelperMock; + private $vaultHelperMock; + private $paymentVaultDeleteToken; + + protected function setUp(): void + { + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->dataHelperMock = $this->createMock(Data::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + $this->requestsHelperMock = $this->createMock(Requests::class); + $this->vaultHelperMock = $this->createMock(\Adyen\Payment\Helper\Vault::class); + + $this->paymentVaultDeleteToken = new PaymentVaultDeleteToken( + $this->storeManagerMock, + $this->dataHelperMock, + $this->adyenLoggerMock, + $this->requestsHelperMock, + $this->vaultHelperMock + ); + } + + public function testSuccessfullyDisableValidAdyenPaymentTokenBeforeDeletion() + { + $paymentTokenMock = $this->createMock(PaymentTokenInterface::class); + $paymentTokenMock->method('getPaymentMethodCode')->willReturn('adyen_cc'); + $paymentTokenMock->method('getEntityId')->willReturn('123'); + + $storeMock = $this->createGeneratedMock(\Magento\Store\Model\Store::class, ['getStoreId']); + $storeMock->method('getStoreId')->willReturn(1); + + $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $storeManagerMock->method('getStore')->willReturn($storeMock); + + $this->vaultHelperMock->method('isAdyenPaymentCode')->willReturn(true); + + $clientMock = $this->createMock(Client::class); + $recurringServiceMock = $this->createMock(Recurring::class); + $this->dataHelperMock->method('initializeAdyenClient')->willReturn($clientMock); + $this->dataHelperMock->method('createAdyenRecurringService')->willReturn($recurringServiceMock); + + $recurringServiceMock->expects($this->once())->method('disable')->willReturn(['response' => 'success']); + + $this->paymentVaultDeleteToken = new PaymentVaultDeleteToken( + $storeManagerMock, + $this->dataHelperMock, + $this->adyenLoggerMock, + $this->requestsHelperMock, + $this->vaultHelperMock + ); + + $result = $this->paymentVaultDeleteToken->beforeDelete($this->createMock(PaymentTokenRepositoryInterface::class), $paymentTokenMock); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertSame([$paymentTokenMock], $result); + } + + public function testHandleExceptionDuringAdyenApiCall() + { + $paymentTokenMock = $this->createMock(PaymentTokenInterface::class); + $paymentTokenMock->method('getPaymentMethodCode')->willReturn('adyen_cc'); + $paymentTokenMock->method('getEntityId')->willReturn('123'); + + $storeMock = $this->createGeneratedMock(\Magento\Store\Model\Store::class, ['getStoreId']); + $storeMock->method('getStoreId')->willReturn(1); + + $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $storeManagerMock->method('getStore')->willReturn($storeMock); + + $this->vaultHelperMock->method('isAdyenPaymentCode')->willReturn(true); + + $this->dataHelperMock->expects($this->once()) + ->method('initializeAdyenClient') + ->willThrowException(new AdyenException('API Error')); + + $this->adyenLoggerMock->expects($this->once()) + ->method('error') + ->with($this->stringContains('Error while attempting to disable token with id 123: API Error')); + + $this->paymentVaultDeleteToken = new PaymentVaultDeleteToken( + $storeManagerMock, + $this->dataHelperMock, + $this->adyenLoggerMock, + $this->requestsHelperMock, + $this->vaultHelperMock + ); + + $result = $this->paymentVaultDeleteToken->beforeDelete($this->createMock(PaymentTokenRepositoryInterface::class), $paymentTokenMock); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertSame([$paymentTokenMock], $result); + } +} diff --git a/composer.json b/composer.json index 9f89055ce..f787681ac 100755 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "adyen/module-payment", "description": "Official Magento2 Plugin to connect to Payment Service Provider Adyen.", "type": "magento2-module", - "version": "9.1.0", + "version": "9.2.0", "license": "MIT", "repositories": [ { diff --git a/etc/config.xml b/etc/config.xml index c1ae64285..24b0ddf34 100755 --- a/etc/config.xml +++ b/etc/config.xml @@ -118,7 +118,7 @@ Stored Payment Methods (Adyen) - 1 + 0 AdyenPaymentIdealFacade pending iDeal @@ -142,7 +142,7 @@ adyen-alternative-payment-method - 1 + 0 AdyenPaymentKlarnaFacade pending Klarna @@ -171,7 +171,7 @@ AdyenPaymentKlarnaVaultFacade - 1 + 0 AdyenPaymentPaypalFacade pending PayPal @@ -200,7 +200,7 @@ AdyenPaymentPaypalVaultFacade - 1 + 0 AdyenPaymentBcmcMobileFacade pending Bancontact Mobile @@ -224,7 +224,7 @@ adyen-alternative-payment-method - 1 + 0 AdyenPaymentDotpayFacade pending DotPay @@ -248,7 +248,7 @@ adyen-alternative-payment-method - 1 + 0 AdyenPaymentAmazonPayFacade pending Amazon Pay @@ -276,7 +276,7 @@ AdyenPaymentAmazonpayVaultFacade - 1 + 0 AdyenPaymentGooglepayFacade pending Google Pay @@ -305,7 +305,7 @@ AdyenPaymentGooglepayVaultFacade - 1 + 0 AdyenPaymentMultibancoFacade pending Multibanco @@ -329,7 +329,7 @@ adyen-alternative-payment-method - 1 + 0 AdyenPaymentFacilypay3xFacade pending FacilyPay 3x @@ -353,7 +353,7 @@ adyen-alternative-payment-method - 1 + 0 AdyenPaymentSepadirectdebitFacade pending SEPA Direct Debit @@ -472,7 +472,7 @@ adyen - 1 + 0 AdyenPaymentApplePayFacade Apple Pay 0 @@ -498,7 +498,7 @@ AdyenPaymentApplePayVaultFacade - 1 + 0 AdyenPaymentGiftcardFacade pending Giftcard @@ -891,6 +891,10 @@ 0 adyen-alternative-payment-method + + Stored Sofort + AdyenPaymentDirectEbankingVaultFacade + 0 AdyenPaymentEbankingFIFacade @@ -1045,6 +1049,10 @@ 0 adyen-alternative-payment-method + + Stored EPS + AdyenPaymentEpsVaultFacade + 0 AdyenPaymentFacilypay4xFacade @@ -1155,6 +1163,10 @@ 0 adyen-alternative-payment-method + + Stored Giropay + AdyenPaymentGiropayVaultFacade + 0 AdyenPaymentGrabpayMYFacade @@ -1243,6 +1255,10 @@ 1 adyen-alternative-payment-method + + Stored KakaoPay + AdyenPaymentKakaopayVaultFacade + 0 AdyenPaymentKlarnaAccountFacade @@ -1265,6 +1281,10 @@ 0 adyen-alternative-payment-method + + Stored Klarna Pay over Time + AdyenPaymentKlarnaAccountVaultFacade + 0 AdyenPaymentKlarnaPaynowFacade @@ -1287,6 +1307,10 @@ 0 adyen-alternative-payment-method + + Stored Klarna Pay Now + AdyenPaymentKlarnaPaynowVaultFacade + 0 AdyenPaymentMbwayFacade @@ -1485,6 +1509,10 @@ 0 adyen-alternative-payment-method + + Stored Open banking + AdyenPaymentPaybybankVaultFacade + 0 AdyenPaymentPixFacade @@ -1618,6 +1646,10 @@ 1 adyen-alternative-payment-method + + Stored TWINT + AdyenPaymentTwintVaultFacade + 0 AdyenPaymentWalleyFacade @@ -1706,6 +1738,10 @@ 1 adyen-alternative-payment-method + + Stored Zip + AdyenPaymentZipVaultFacade + 0 AdyenPaymentGcashFacade @@ -1728,6 +1764,10 @@ 1 adyen-alternative-payment-method + + Stored GCash + AdyenPaymentGcashVaultFacade + 0 AdyenPaymentOxxoFacade @@ -1794,6 +1834,10 @@ 0 adyen-alternative-payment-method + + Stored Carnet + AdyenPaymentCarnetVaultFacade + 0 AdyenPaymentWalleyB2bFacade @@ -1860,6 +1904,10 @@ 1 adyen-alternative-payment-method + + Stored MoMo Wallet + AdyenPaymentMomoWalletVaultFacade + 0 AdyenPaymentTouchngoFacade @@ -1904,6 +1952,36 @@ 0 adyen-alternative-payment-method + + Stored Bancontact + AdyenPaymentBcmcVaultFacade + + + 0 + AdyenPaymentCashappFacade + Cash App Pay + 0 + 0 + authorize + 1 + 1 + 1 + 0 + 0 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 0 + adyen-alternative-payment-method + + + Stored Cash App Pay + AdyenPaymentCashappVaultFacade + 0 AdyenPaymentKlarnaB2bFacade @@ -1926,6 +2004,50 @@ 0 adyen-alternative-payment-method + + 0 + AdyenPaymentMobilepayFacade + MobilePay + 0 + 0 + authorize + 1 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + adyen-alternative-payment-method + + + 0 + AdyenPaymentVippsFacade + Vipps + 0 + 0 + authorize + 1 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + adyen-alternative-payment-method + - \ No newline at end of file + diff --git a/etc/di.xml b/etc/di.xml index 8e5800d17..cd6a24ad2 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -812,7 +812,21 @@ AdyenPaymentCommandManager AdyenPaymentCommandManager AdyenPaymentCommandManager + AdyenPaymentCommandManager AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + AdyenPaymentCommandManager + @@ -1655,6 +1669,7 @@ + @@ -3828,6 +3843,59 @@ adyen_bcmc_vault + + + adyen_cashapp + Magento\Payment\Block\Form + Adyen\Payment\Block\Info\PaymentMethodInfo + AdyenPaymentCashappValueHandlerPool + AdyenPaymentValidatorPool + AdyenPaymentCommandPool + + + + + + AdyenPaymentCashappConfigValueHandler + + + + + + AdyenPaymentCashappConfig + + + + + adyen_cashapp + + + + + adyen_cashapp_vault + AdyenPaymentCashappVaultConfig + AdyenPaymentCashappVaultValueHandlerPool + AdyenPaymentCashappFacade + AdyenPaymentCommandPool + + + + + + AdyenPaymentCashappVaultConfigValueHandler + + + + + + AdyenPaymentCashappVaultConfig + + + + + adyen_cashapp_vault + + adyen_klarna_b2b @@ -3855,4 +3923,59 @@ adyen_klarna_b2b - \ No newline at end of file + + + adyen_mobilepay + Magento\Payment\Block\Form + Adyen\Payment\Block\Info\PaymentMethodInfo + AdyenPaymentMobilepayValueHandlerPool + AdyenPaymentValidatorPool + AdyenPaymentCommandPool + + + + + adyen_vipps + Magento\Payment\Block\Form + Adyen\Payment\Block\Info\PaymentMethodInfo + AdyenPaymentVippsValueHandlerPool + AdyenPaymentValidatorPool + AdyenPaymentCommandPool + + + + + + AdyenPaymentMobilepayConfigValueHandler + + + + + + AdyenPaymentMobilepayConfig + + + + + adyen_mobilepay + + + + + + AdyenPaymentVippsConfigValueHandler + + + + + + AdyenPaymentVippsConfig + + + + + adyen_vipps + + + + diff --git a/etc/events.xml b/etc/events.xml index cc68373a4..2580f623d 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -266,7 +266,19 @@ + + + - \ No newline at end of file + + + + + + + + + + diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml index 092deb2d8..e45441148 100755 --- a/etc/frontend/di.xml +++ b/etc/frontend/di.xml @@ -100,7 +100,10 @@ touchngo boletobancario bcmc + cashapp klarna_b2b + mobilepay + vipps Adyen_Payment/js/view/payment/method-renderer/adyen-cc-method @@ -112,6 +115,7 @@ Adyen_Payment/js/view/payment/method-renderer/adyen-paypal-method Adyen_Payment/js/view/payment/method-renderer/adyen-giftcard-method Adyen_Payment/js/view/payment/method-renderer/adyen-affirm-method + Adyen_Payment/js/view/payment/method-renderer/adyen-cashapp-method Adyen_Payment/js/view/payment/method-renderer/adyen-ratepay-method Adyen_Payment/js/view/payment/method-renderer/adyen-ratepay-directdebit-method Adyen_Payment/js/view/payment/method-renderer/adyen-pos-cloud-method @@ -142,7 +146,8 @@ Adyen\Payment\Model\Ui\CardTokenUiComponentProvider Adyen\Payment\Model\Ui\CardTokenUiComponentProvider Adyen\Payment\Model\Ui\PaymentMethodTokenUiComponentProvider + Adyen\Payment\Model\Ui\PaymentMethodTokenUiComponentProvider - \ No newline at end of file + diff --git a/etc/graphql/di.xml b/etc/graphql/di.xml index 92b499da8..13bd54d79 100644 --- a/etc/graphql/di.xml +++ b/etc/graphql/di.xml @@ -80,7 +80,10 @@ Adyen\Payment\Model\Cart\Payment\AdditionalDataProvider\AdyenPm Adyen\Payment\Model\Cart\Payment\AdditionalDataProvider\AdyenPm Adyen\Payment\Model\Cart\Payment\AdditionalDataProvider\AdyenPm + Adyen\Payment\Model\Cart\Payment\AdditionalDataProvider\AdyenPm Adyen\Payment\Model\Cart\Payment\AdditionalDataProvider\AdyenPm + Adyen\Payment\Model\Cart\Payment\AdditionalDataProvider\AdyenPm + Adyen\Payment\Model\Cart\Payment\AdditionalDataProvider\AdyenPm diff --git a/etc/module.xml b/etc/module.xml index 3efcc97e4..867f5452b 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -12,7 +12,7 @@ --> - + diff --git a/etc/payment.xml b/etc/payment.xml index 8cc0ea9ca..3dd06ebd5 100755 --- a/etc/payment.xml +++ b/etc/payment.xml @@ -230,8 +230,17 @@ 1 + + 1 + 1 + + 1 + + + 1 + \ No newline at end of file diff --git a/etc/webapi.xml b/etc/webapi.xml index 37435e130..dbe727ead 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -130,4 +130,14 @@ + + + + + + + + %customer_id% + + diff --git a/view/base/web/images/logos/cashapp.svg b/view/base/web/images/logos/cashapp.svg new file mode 100644 index 000000000..b2dbe1cb4 --- /dev/null +++ b/view/base/web/images/logos/cashapp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view/base/web/images/logos/mobilepay.svg b/view/base/web/images/logos/mobilepay.svg new file mode 100644 index 000000000..cad07f183 --- /dev/null +++ b/view/base/web/images/logos/mobilepay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view/base/web/images/logos/vipps.svg b/view/base/web/images/logos/vipps.svg new file mode 100644 index 000000000..020a18ddc --- /dev/null +++ b/view/base/web/images/logos/vipps.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml index 83cbd6e05..e634d30d2 100755 --- a/view/frontend/layout/checkout_index_index.xml +++ b/view/frontend/layout/checkout_index_index.xml @@ -281,9 +281,18 @@ true + + true + true + + true + + + true + @@ -301,4 +310,4 @@ - \ No newline at end of file + diff --git a/view/frontend/layout/multishipping_checkout_billing.xml b/view/frontend/layout/multishipping_checkout_billing.xml index 6be38d8e8..629e7db0e 100644 --- a/view/frontend/layout/multishipping_checkout_billing.xml +++ b/view/frontend/layout/multishipping_checkout_billing.xml @@ -90,7 +90,10 @@ Adyen_Payment::form/multishipping/abstract-form.phtml Adyen_Payment::form/multishipping/abstract-form.phtml Adyen_Payment::form/multishipping/abstract-form.phtml + Adyen_Payment::form/multishipping/abstract-form.phtml Adyen_Payment::form/multishipping/abstract-form.phtml + Adyen_Payment::form/multishipping/abstract-form.phtml + Adyen_Payment::form/multishipping/abstract-form.phtml false diff --git a/view/frontend/web/js/view/payment/method-renderer/adyen-cashapp-method.js b/view/frontend/web/js/view/payment/method-renderer/adyen-cashapp-method.js new file mode 100644 index 000000000..cc9ac5ce7 --- /dev/null +++ b/view/frontend/web/js/view/payment/method-renderer/adyen-cashapp-method.js @@ -0,0 +1,38 @@ +/** + * + * Adyen Payment module (https://www.adyen.com/) + * + * Copyright (c) 2023 Adyen NV (https://www.adyen.com/) + * See LICENSE.txt for license details. + * + * Author: Adyen + */ +define( + [ + 'Adyen_Payment/js/view/payment/method-renderer/adyen-pm-method', + 'Magento_Checkout/js/model/full-screen-loader', + ], + function( + adyenPaymentMethod, + fullScreenLoader + ) { + return adyenPaymentMethod.extend({ + placeOrderButtonVisible: false, + initialize: function () { + this._super(); + }, + buildComponentConfiguration: function (paymentMethod, paymentMethodsExtraInfo) { + let baseComponentConfiguration = this._super(); + let cashAppConfiguration = Object.assign(baseComponentConfiguration, paymentMethodsExtraInfo[paymentMethod.type].configuration); + cashAppConfiguration.showPayButton = true; + cashAppConfiguration.enableStoreDetails = true; + return cashAppConfiguration + }, + renderActionComponent: function(resultCode, action, component) { + fullScreenLoader.stopLoader(); + + this.actionComponent = component.handleAction(action); + }, + }) + } +); diff --git a/view/frontend/web/js/view/payment/method-renderer/adyen-pm-method.js b/view/frontend/web/js/view/payment/method-renderer/adyen-pm-method.js index b97226d77..e9cec9d67 100755 --- a/view/frontend/web/js/view/payment/method-renderer/adyen-pm-method.js +++ b/view/frontend/web/js/view/payment/method-renderer/adyen-pm-method.js @@ -211,7 +211,8 @@ define( self.isPlaceOrderAllowed(state.isValid); }, }); - return configuration + + return configuration; }, getTxVariant: function () { diff --git a/view/frontend/web/template/payment/card-vault-form.html b/view/frontend/web/template/payment/card-vault-form.html index af5eb04ae..31327f7b2 100644 --- a/view/frontend/web/template/payment/card-vault-form.html +++ b/view/frontend/web/template/payment/card-vault-form.html @@ -51,7 +51,7 @@
- +
diff --git a/view/frontend/web/template/payment/pm-form.html b/view/frontend/web/template/payment/pm-form.html index 49369f01b..fbe8d6dd4 100755 --- a/view/frontend/web/template/payment/pm-form.html +++ b/view/frontend/web/template/payment/pm-form.html @@ -19,7 +19,7 @@
-
+