From 752f83b842b985d7d371ecf7565a7665fd99a40b Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Wed, 5 Mar 2025 14:59:28 +0100 Subject: [PATCH 01/42] [ECP-9593] Implement an abstract handler for order status history --- .../AbstractOrderStatusHistoryHandler.php | 55 +++++++++++++++++++ Helper/OrderStatusHistory.php | 37 +++++++++++++ etc/di.xml | 28 ++++++++-- 3 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 Gateway/Response/AbstractOrderStatusHistoryHandler.php create mode 100644 Helper/OrderStatusHistory.php diff --git a/Gateway/Response/AbstractOrderStatusHistoryHandler.php b/Gateway/Response/AbstractOrderStatusHistoryHandler.php new file mode 100644 index 000000000..01d26bf5a --- /dev/null +++ b/Gateway/Response/AbstractOrderStatusHistoryHandler.php @@ -0,0 +1,55 @@ + + */ + +namespace Adyen\Payment\Gateway\Response; + +use Adyen\AdyenException; +use Adyen\Payment\Helper\OrderStatusHistory; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; + +class AbstractOrderStatusHistoryHandler implements HandlerInterface +{ + /** + * @param string $eventType Indicates the API call event type (authorization, capture etc.) + * @param OrderStatusHistory $orderStatusHistoryHelper + */ + public function __construct( + protected readonly string $eventType, + protected readonly OrderStatusHistory $orderStatusHistoryHelper + ) { } + + /** + * @throws AdyenException + */ + public function handle(array $handlingSubject, array $responseCollection): void + { + if (empty($this->eventType)) { + throw new AdyenException( + __('Order status history can not be handled due to missing event type!') + ); + } + + $readPayment = SubjectReader::readPayment($handlingSubject); + $payment = $readPayment->getPayment(); + $order = $payment->getOrder(); + + // Temporary workaround to clean-up `hasOnlyGiftCards` key. It needs to be handled separately. + if (isset($responseCollection['hasOnlyGiftCards'])) { + unset($responseCollection['hasOnlyGiftCards']); + } + + foreach ($responseCollection as $response) { + $comment = $this->orderStatusHistoryHelper->buildComment($response, $this->eventType); + $order->addCommentToStatusHistory($comment, $order->getStatus()); + } + } +} diff --git a/Helper/OrderStatusHistory.php b/Helper/OrderStatusHistory.php new file mode 100644 index 000000000..795d040b5 --- /dev/null +++ b/Helper/OrderStatusHistory.php @@ -0,0 +1,37 @@ + + */ + +namespace Adyen\Payment\Helper; + +class OrderStatusHistory +{ + public function buildComment(array $response, string $actionName): String + { + $comment = __("Adyen %1 API response:", $actionName) . '
'; + + if (isset($response['resultCode'])) { + $comment .= __("Result Code: %1", $response['resultCode']) . '
'; + } + + // Modification responses contain `status` but not `resultCode`. + if (isset($response['status'])) { + $comment .= __("Status: %1", $response['status']) . '
'; + } + + if (isset($response['pspReference'])) { + $comment .= __("PSP reference: %1", $response['pspReference']); + } + + $comment .= '
'; + + return $comment; + } +} diff --git a/etc/di.xml b/etc/di.xml index 7f7dc2329..93b0d4e7f 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1334,11 +1334,31 @@ Adyen\Payment\Gateway\Response\CheckoutPaymentsDetailsHandler Adyen\Payment\Gateway\Response\VaultDetailsHandler - Adyen\Payment\Gateway\Response\CheckoutPaymentCommentHistoryHandler + AdyenPaymentAuthorisationOrderStatusHistoryHandler Adyen\Payment\Gateway\Response\StateDataHandler + + + authorisation + + + + + capture + + + + + refund + + + + + cancellation + + @@ -1359,7 +1379,7 @@ Adyen\Payment\Gateway\Response\PaymentCaptureDetailsHandler - Adyen\Payment\Gateway\Response\PaymentCommentHistoryHandler + AdyenPaymentCaptureOrderStatusHistoryHandler @@ -1367,7 +1387,7 @@ Adyen\Payment\Gateway\Response\PaymentRefundDetailsHandler - Adyen\Payment\Gateway\Response\PaymentCommentHistoryRefundHandler + AdyenPaymentRefundOrderStatusHistoryHandler @@ -1375,7 +1395,7 @@ Adyen\Payment\Gateway\Response\PaymentCancelDetailsHandler - Adyen\Payment\Gateway\Response\PaymentCommentHistoryHandler + AdyenPaymentCancellationOrderStatusHistoryHandler From e505f017e238d6e80f43f83007240f5019cd00fb Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 10 Mar 2025 14:29:27 +0100 Subject: [PATCH 02/42] [ECP-9593] Revert changes related to CAPTURE response handler --- etc/di.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/di.xml b/etc/di.xml index 93b0d4e7f..6d52bc461 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1379,7 +1379,7 @@ Adyen\Payment\Gateway\Response\PaymentCaptureDetailsHandler - AdyenPaymentCaptureOrderStatusHistoryHandler + Adyen\Payment\Gateway\Response\PaymentCommentHistoryHandler From 7b8d2139c4111d59db6356bf9555f2cb56cc463c Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 10 Mar 2025 14:30:05 +0100 Subject: [PATCH 03/42] [ECP-9593] Implement comment builder for webhooks --- Helper/PaymentResponseHandler.php | 115 ++++++++++++------------------ 1 file changed, 45 insertions(+), 70 deletions(-) diff --git a/Helper/PaymentResponseHandler.php b/Helper/PaymentResponseHandler.php index d74b3e1d6..218d31c1c 100644 --- a/Helper/PaymentResponseHandler.php +++ b/Helper/PaymentResponseHandler.php @@ -12,6 +12,7 @@ namespace Adyen\Payment\Helper; use Adyen\Model\Checkout\CancelOrderRequest; +use Adyen\Payment\Helper\Order as AdyenOrderHelper; use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\ResourceModel\PaymentResponse\CollectionFactory as PaymentResponseCollectionFactory; use Exception; @@ -50,48 +51,35 @@ class PaymentResponseHandler ]; /** - * @var AdyenLogger + * @param AdyenLogger $adyenLogger + * @param Vault $vaultHelper + * @param Order $orderResourceModel + * @param Data $dataHelper + * @param Quote $quoteHelper + * @param AdyenOrderHelper $orderHelper + * @param OrderRepository $orderRepository + * @param HistoryFactory $orderHistoryFactory + * @param StateData $stateDataHelper + * @param PaymentResponseCollectionFactory $paymentResponseCollectionFactory + * @param Config $configHelper + * @param PaymentMethods $paymentMethodsHelper + * @param OrderStatusHistory $orderStatusHistoryHelper */ - private AdyenLogger $adyenLogger; - private Vault $vaultHelper; - private Order $orderResourceModel; - private Data $dataHelper; - private Quote $quoteHelper; - private \Adyen\Payment\Helper\Order $orderHelper; - private OrderRepository $orderRepository; - private HistoryFactory $orderHistoryFactory; - private StateData $stateDataHelper; - private PaymentResponseCollectionFactory $paymentResponseCollectionFactory; - private Config $configHelper; - private PaymentMethods $paymentMethodsHelper; - public function __construct( - AdyenLogger $adyenLogger, - Vault $vaultHelper, - Order $orderResourceModel, - Data $dataHelper, - Quote $quoteHelper, - \Adyen\Payment\Helper\Order $orderHelper, - OrderRepository $orderRepository, - HistoryFactory $orderHistoryFactory, - StateData $stateDataHelper, - PaymentResponseCollectionFactory $paymentResponseCollectionFactory, - Config $configHelper, - PaymentMethods $paymentMethodsHelper - ) { - $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; - $this->paymentResponseCollectionFactory = $paymentResponseCollectionFactory; - $this->configHelper = $configHelper; - $this->paymentMethodsHelper = $paymentMethodsHelper; - } + private readonly AdyenLogger $adyenLogger, + private readonly Vault $vaultHelper, + private readonly Order $orderResourceModel, + private readonly Data $dataHelper, + private readonly Quote $quoteHelper, + private readonly AdyenOrderHelper $orderHelper, + private readonly OrderRepository $orderRepository, + private readonly HistoryFactory $orderHistoryFactory, + private readonly StateData $stateDataHelper, + private readonly PaymentResponseCollectionFactory $paymentResponseCollectionFactory, + private readonly Config $configHelper, + private readonly PaymentMethods $paymentMethodsHelper, + private readonly OrderStatusHistory $orderStatusHistoryHelper + ) { } public function formatPaymentResponse( string $resultCode, @@ -170,23 +158,6 @@ public function handlePaymentsDetailsResponse( 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 - ); - $resultCode = $paymentsDetailsResponse['resultCode']; if (!empty($resultCode)) { $payment->setAdditionalInformation('resultCode', $resultCode); @@ -234,9 +205,15 @@ public function handlePaymentsDetailsResponse( * Otherwise keep the order state as pending_payment. */ $order = $this->orderHelper->setStatusOrderCreation($order); - $this->orderRepository->save($order); } + // Add order status history comment for /payments/details API response + $comment = $this->orderStatusHistoryHelper->buildApiResponseComment( + $paymentsDetailsResponse, + 'payments/details' + ); + $order->addCommentToStatusHistory($comment, $order->getStatus()); + // Cleanup state data if exists. try { $this->stateDataHelper->cleanQuoteStateData($order->getQuoteId(), $authResult); @@ -244,6 +221,10 @@ public function handlePaymentsDetailsResponse( $this->adyenLogger->error(__('Error cleaning the payment state data: %s', $exception->getMessage())); } + $paymentMethod = $paymentsDetailsResponse['paymentMethod']['brand'] ?? + $paymentsDetailsResponse['paymentMethod']['type'] ?? + ''; + switch ($resultCode) { case self::AUTHORISED: if (!empty($paymentsDetailsResponse['pspReference'])) { @@ -273,16 +254,18 @@ public function handlePaymentsDetailsResponse( // do nothing wait for the notification if (strpos((string) $paymentMethod, "bankTransfer") !== false) { - $comment .= "

Waiting for the customer to transfer the money."; + $pendingComment = "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."; + $pendingComment = "This request will be send to the bank at the end of the day."; } else { - $comment .= "

The payment result is not confirmed (yet). + $pendingComment = "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."; } + + $order->addCommentToStatusHistory($pendingComment, $order->getStatus()); $this->adyenLogger->addAdyenResult('Do nothing wait for the notification'); $result = true; @@ -318,7 +301,7 @@ public function handlePaymentsDetailsResponse( if ($order->canCancel()) { // Proceed to set cancellation action flag and cancel the order $order->setActionFlag(\Magento\Sales\Model\Order::ACTION_FLAG_CANCEL, true); - $this->dataHelper->cancelOrder($order); + $this->dataHelper->cancelOrder($order, $resultCode); } else { $this->adyenLogger->addAdyenResult('The order cannot be cancelled'); } @@ -337,14 +320,6 @@ public function handlePaymentsDetailsResponse( break; } - $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); From 848d53e8744a51802ae998fc706a4cbb3566ca50 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 10 Mar 2025 14:30:20 +0100 Subject: [PATCH 04/42] [ECP-9593] Refactor the renamed method --- Gateway/Response/AbstractOrderStatusHistoryHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gateway/Response/AbstractOrderStatusHistoryHandler.php b/Gateway/Response/AbstractOrderStatusHistoryHandler.php index 01d26bf5a..536c187a9 100644 --- a/Gateway/Response/AbstractOrderStatusHistoryHandler.php +++ b/Gateway/Response/AbstractOrderStatusHistoryHandler.php @@ -48,7 +48,7 @@ public function handle(array $handlingSubject, array $responseCollection): void } foreach ($responseCollection as $response) { - $comment = $this->orderStatusHistoryHelper->buildComment($response, $this->eventType); + $comment = $this->orderStatusHistoryHelper->buildApiResponseComment($response, $this->eventType); $order->addCommentToStatusHistory($comment, $order->getStatus()); } } From 37dcfde8109c94a9b6d86fe959ab0e2d54c2dfb3 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 10 Mar 2025 14:30:47 +0100 Subject: [PATCH 05/42] [ECP-9593] Improve order cancellation message and add optional reason --- Helper/Data.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Helper/Data.php b/Helper/Data.php index b5116c956..34cdb2048 100755 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -561,8 +561,9 @@ public function getLiveEndpointPrefix($storeId = null) * Cancels the order * * @param $order + * @param null $reason */ - public function cancelOrder($order) + public function cancelOrder($order, $reason = null) { $orderStatus = $this->configHelper->getAdyenAbstractConfigData('payment_cancelled'); $order->setActionFlag($orderStatus, true); @@ -577,12 +578,11 @@ public function cancelOrder($order) if ($order->canCancel()) { if ($this->orderManagement->cancel($order->getEntityId())) { //new canceling process try { - $orderStatusHistory = $this->orderStatusHistoryFactory->create() - ->setParentId($order->getEntityId()) - ->setEntityName('order') - ->setStatus(Order::STATE_CANCELED) - ->setComment(__('Order has been cancelled by "%1" payment response.', $order->getPayment()->getMethod())); - $this->orderManagement->addComment($order->getEntityId(), $orderStatusHistory); + $comment = __('Order has been cancelled.', $order->getPayment()->getMethod()); + if ($reason) { + $comment .= '
' . __("Reason: %1", $reason) . '
'; + } + $order->addCommentToStatusHistory($comment, $order->getStatus()); } catch (Exception $e) { $this->adyenLogger->addAdyenDebug( __('Order cancel history comment error: %1', $e->getMessage()), From a0785659d34e380fe056e6d945c60ccfb971028f Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 10 Mar 2025 14:31:23 +0100 Subject: [PATCH 06/42] [ECP-9593] Add comment builder for webhooks --- Helper/OrderStatusHistory.php | 113 ++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/Helper/OrderStatusHistory.php b/Helper/OrderStatusHistory.php index 795d040b5..a56633ad9 100644 --- a/Helper/OrderStatusHistory.php +++ b/Helper/OrderStatusHistory.php @@ -11,14 +11,34 @@ namespace Adyen\Payment\Helper; +use Adyen\Payment\Api\Data\NotificationInterface; +use Adyen\Payment\Model\Notification; +use Magento\Sales\Api\Data\OrderInterface; + class OrderStatusHistory { - public function buildComment(array $response, string $actionName): String + /** + * @param ChargedCurrency $chargedCurrencyHelper + * @param Data $adyenHelper + */ + public function __construct( + private readonly ChargedCurrency $chargedCurrencyHelper, + private readonly Data $adyenHelper + ) { } + + /** + * Builds the order status history comment based on the Checkout API response + * + * @param array $response + * @param string $actionName + * @return string + */ + public function buildApiResponseComment(array $response, string $actionName): string { - $comment = __("Adyen %1 API response:", $actionName) . '
'; + $comment = '' . __("Adyen %1 response", $actionName) . '
'; if (isset($response['resultCode'])) { - $comment .= __("Result Code: %1", $response['resultCode']) . '
'; + $comment .= __("Result code: %1", $response['resultCode']) . '
'; } // Modification responses contain `status` but not `resultCode`. @@ -27,11 +47,94 @@ public function buildComment(array $response, string $actionName): String } if (isset($response['pspReference'])) { - $comment .= __("PSP reference: %1", $response['pspReference']); + $comment .= __("PSP reference: %1", $response['pspReference']) . '
'; + } + + if ($paymentMethod = $response['paymentMethod']['brand'] ?? $response['paymentMethod']['type'] ?? null) { + $comment .= __("Payment method: %1", $paymentMethod) . '
'; } - $comment .= '
'; + if (isset($response['refusalReason'])) { + $comment .= __("Refusal reason: %1", $response['refusalReason']) . '
'; + } + + if (isset($response['errorCode'])) { + $comment .= __("Error code: %1", $response['errorCode']) . '
'; + } return $comment; } + + /** + * Builds the order status history comment based on the webhook + * + * @param OrderInterface $order + * @param NotificationInterface $notification + * @param string|null $klarnaReservationNumber + * @return string + */ + public function buildWebhookComment( + OrderInterface $order, + NotificationInterface $notification, + ?string $klarnaReservationNumber = null, + ): string { + $comment = '' . __( + "Adyen %1%2 webhook", + $this->getIsWebhookForPartialPayment($order, $notification) ? 'partial ': '', + $notification->getEventCode() + ) . '
'; + + if (!empty($notification->getPspreference())) { + $comment .= __("PSP reference: %1", $notification->getPspreference()) . '
'; + } + + if (!empty($notification->getPaymentMethod())) { + $comment .= __("Payment method: %1", $notification->getPaymentMethod()) . '
'; + } + + if (!empty($notification->getSuccess())) { + $status = $notification->isSuccessful() ? 'Successful' : 'Failed'; + $comment .= __("Event status: %1", $status) . '
'; + } + + if (!empty($notification->getReason())) { + $comment .= __("Reason: %1", $notification->getReason()) . '
'; + } + + if (isset($klarnaReservationNumber)) { + $comment .= __("Reservation number: %1", $klarnaReservationNumber) . '
'; + } + + return $comment; + } + + /** + * Identifies whether if the notification belongs to a partial modification or not + * + * @param OrderInterface $order + * @param NotificationInterface $notification + * @return bool + */ + private function getIsWebhookForPartialPayment( + OrderInterface $order, + NotificationInterface $notification + ): bool { + $isPartial = false; + + if (in_array($notification->getEventCode(), [Notification::REFUND, Notification::CAPTURE])) { + // check if it is a full or partial refund + $amount = $notification->getAmountValue(); + $orderAmountCurrency = $this->chargedCurrencyHelper->getOrderAmountCurrency($order, false); + $formattedOrderAmount = $this->adyenHelper->formatAmount( + $orderAmountCurrency->getAmount(), + $orderAmountCurrency->getCurrencyCode() + ); + + if ($amount !== $formattedOrderAmount) { + $isPartial = true; + } + } + + return $isPartial; + } } From 04d85016a2175a55c9e1a30a993b78d370ae355d Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 10 Mar 2025 14:32:21 +0100 Subject: [PATCH 07/42] [ECP-9593] Improve object handling on capture webhook handling process and revisit the comments --- Helper/Invoice.php | 17 +++-- Helper/Webhook.php | 83 ++---------------------- Helper/Webhook/CaptureWebhookHandler.php | 8 +-- 3 files changed, 22 insertions(+), 86 deletions(-) diff --git a/Helper/Invoice.php b/Helper/Invoice.php index 75183d1fd..30e6df3f4 100644 --- a/Helper/Invoice.php +++ b/Helper/Invoice.php @@ -219,11 +219,11 @@ public function createAdyenInvoice( * * @param Order $order * @param Notification $notification - * @return AdyenInvoice + * @return array + * @throws AdyenWebhookException * @throws AlreadyExistsException - * @throws Exception */ - public function handleCaptureWebhook(Order $order, Notification $notification): AdyenInvoice + public function handleCaptureWebhook(Order $order, Notification $notification): array { $invoiceFactory = $this->adyenInvoiceFactory->create(); $adyenInvoice = $this->adyenInvoiceResourceModel->getAdyenInvoiceByCaptureWebhook($order, $notification); @@ -280,12 +280,19 @@ public function handleCaptureWebhook(Order $order, Notification $notification): $magentoInvoice = $this->magentoInvoiceFactory->create()->load($adyenInvoiceObject->getInvoiceId()); if ($this->isFullInvoiceAmountManuallyCaptured($magentoInvoice)) { + /* + * Magento Invoice updates the Order object while paying the invoice. This creates two divergent + * Order objects. In the downstream, some information might be missing due to setting them on the + * wrong order object as the Order might be already updated in the upstream without persisting + * it to the database. Setting the order again on the Invoice makes sure we are dealing + * with the same order object always. + */ + $magentoInvoice->setOrder($order); $magentoInvoice->pay(); $this->invoiceRepository->save($magentoInvoice); - $this->magentoOrderResourceModel->save($magentoInvoice->getOrder()); } - return $adyenInvoiceObject; + return [$adyenInvoiceObject, $magentoInvoice, $order]; } /** diff --git a/Helper/Webhook.php b/Helper/Webhook.php index ddaac33fb..57b2b10db 100644 --- a/Helper/Webhook.php +++ b/Helper/Webhook.php @@ -51,34 +51,28 @@ class Webhook 'payment_authorized' => [Order::STATE_PROCESSING] ]; - // TODO::This property is not written but only is read. Check the usage. - private $boletoPaidAmount; private ?string $klarnaReservationNumber; private ?string $ratepayDescriptor; /** - * @param Data $adyenHelper * @param SerializerInterface $serializer * @param TimezoneInterface $timezone * @param Config $configHelper - * @param ChargedCurrency $chargedCurrency * @param AdyenLogger $logger * @param WebhookHandlerFactory $webhookHandlerFactory * @param OrderHelper $orderHelper * @param OrderRepository $orderRepository - * @param PaymentMethods $paymentMethodsHelper + * @param OrderStatusHistory $orderStatusHistoryHelper */ public function __construct( - private readonly Data $adyenHelper, private readonly SerializerInterface $serializer, private readonly TimezoneInterface $timezone, private readonly ConfigHelper $configHelper, - private readonly ChargedCurrency $chargedCurrency, private readonly AdyenLogger $logger, private readonly WebhookHandlerFactory $webhookHandlerFactory, private readonly OrderHelper $orderHelper, private readonly OrderRepository $orderRepository, - private readonly PaymentMethods $paymentMethodsHelper + private readonly OrderStatusHistory $orderStatusHistoryHelper ) { $this->klarnaReservationNumber = null; $this->ratepayDescriptor = null; @@ -287,76 +281,17 @@ private function declareVariables(Notification $notification): void private function addNotificationDetailsHistoryComment(Order $order, Notification $notification): Order { - $successResult = $notification->isSuccessful() ? 'true' : 'false'; - $reason = $notification->getReason(); - $success = (!empty($reason)) ? "$successResult
reason:$reason" : $successResult; - - $payment = $order->getPayment(); - $paymentMethodInstance = $payment->getMethodInstance(); - - $eventCode = $notification->getEventCode(); - if ($eventCode == Notification::REFUND || $eventCode == Notification::CAPTURE) { - // check if it is a full or partial refund - $amount = $notification->getAmountValue(); - $orderAmountCurrency = $this->chargedCurrency->getOrderAmountCurrency($order, false); - $formattedOrderAmount = $this->adyenHelper - ->formatAmount($orderAmountCurrency->getAmount(), $orderAmountCurrency->getCurrencyCode()); - - if ($amount == $formattedOrderAmount) { - $order->setData( - 'adyen_notification_event_code', - $eventCode . " : " . strtoupper($successResult) - ); - } else { - $order->setData( - 'adyen_notification_event_code', - "(PARTIAL) " . - $eventCode . " : " . strtoupper($successResult) - ); - } - } else { - $order->setData( - 'adyen_notification_event_code', - $eventCode . " : " . strtoupper($successResult) - ); - } - - // if payment method is klarna, ratepay or openinvoice/afterpay show the reservartion number - if ($this->paymentMethodsHelper->isOpenInvoice($paymentMethodInstance) && - !empty($this->klarnaReservationNumber)) { - $klarnaReservationNumberText = "
reservationNumber: " . $this->klarnaReservationNumber; - } else { - $klarnaReservationNumberText = ""; - } - - if ($this->boletoPaidAmount != null && $this->boletoPaidAmount != "") { - $boletoPaidAmountText = "
Paid amount: " . $this->boletoPaidAmount; - } else { - $boletoPaidAmountText = ""; - } - - $type = 'Adyen HTTP Notification(s):'; - $comment = __( - '%1
eventCode: %2
pspReference: %3
paymentMethod: %4
' . - ' success: %5 %6 %7', - $type, - $eventCode, - $notification->getPspreference(), - $notification->getPaymentMethod(), - $success, - $klarnaReservationNumberText, - $boletoPaidAmountText - ); + $comment = $this->orderStatusHistoryHelper->buildWebhookComment($order, $notification); // If notification is pending status and pending status is set add the status change to the comment history - if ($eventCode == Notification::PENDING) { + if ($notification->getEventCode() == Notification::PENDING) { $pendingStatus = $this->configHelper->getConfigData( 'pending_status', 'adyen_abstract', $order->getStoreId() ); if ($pendingStatus != "") { - $order->addStatusHistoryComment($comment, $pendingStatus); + $order->addCommentToStatusHistory($comment, $pendingStatus); $this->logger->addAdyenNotification( 'Created comment history for this notification with status change to: ' . $pendingStatus, array_merge( @@ -368,7 +303,7 @@ private function addNotificationDetailsHistoryComment(Order $order, Notification } } - $order->addStatusHistoryComment($comment, $order->getStatus()); + $order->addCommentToStatusHistory($comment, $order->getStatus()); $this->logger->addAdyenNotification( 'Created comment history for this notification', [ @@ -465,10 +400,6 @@ private function updateOrderPaymentWithAdyenAttributes( ); } - if ($this->boletoPaidAmount != "") { - $payment->setAdditionalInformation('adyen_boleto_paid_amount', $this->boletoPaidAmount); - } - if ($this->ratepayDescriptor !== "") { $payment->setAdditionalInformation( 'adyen_ratepay_descriptor', @@ -521,7 +452,7 @@ private function setNotificationError(Notification $notification, string $errorM private function addNotificationErrorComment(Order $order, string $errorMessage): Order { $comment = __('The order failed to update: %1', $errorMessage); - $order->addStatusHistoryComment($comment, $order->getStatus()); + $order->addCommentToStatusHistory($comment, $order->getStatus()); $this->orderRepository->save($order); return $order; } diff --git a/Helper/Webhook/CaptureWebhookHandler.php b/Helper/Webhook/CaptureWebhookHandler.php index 77914f851..9504a60b2 100644 --- a/Helper/Webhook/CaptureWebhookHandler.php +++ b/Helper/Webhook/CaptureWebhookHandler.php @@ -93,10 +93,9 @@ public function handleWebhook(MagentoOrder $order, Notification $notification, s return $order; } - // TODO Get functionality out of the invoiceHelper function, so we don't have to fetch the order from the db - $adyenInvoice = $this->invoiceHelper->handleCaptureWebhook($order, $notification); - // Refresh the order by fetching it from the db - $order = $this->orderHelper->fetchOrderByIncrementId($notification); + list($adyenInvoice, $magentoInvoice, $order) = + $this->invoiceHelper->handleCaptureWebhook($order, $notification); + $adyenOrderPayment = $this->adyenOrderPaymentFactory->create()->load($adyenInvoice->getAdyenPaymentOrderId(), OrderPaymentInterface::ENTITY_ID); $this->adyenOrderPaymentHelper->refreshPaymentCaptureStatus($adyenOrderPayment, $notification->getAmountCurrency()); $this->adyenLogger->addAdyenNotification( @@ -112,7 +111,6 @@ public function handleWebhook(MagentoOrder $order, Notification $notification, s ] ); - $magentoInvoice = $this->magentoInvoiceFactory->create()->load($adyenInvoice->getInvoiceId(), MagentoInvoice::ENTITY_ID); $this->adyenLogger->addAdyenNotification( sprintf('Notification %s updated invoice {invoiceId}', $notification->getEntityId()), array_merge( From e58a17c287e06818f3fc4674b5fd252504bd5e81 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 10 Mar 2025 15:30:14 +0100 Subject: [PATCH 08/42] [ECP-9593] Remove CheckoutPaymentCommentHistoryHandler class --- .../CheckoutPaymentCommentHistoryHandler.php | 47 ------- .../CheckoutPaymentsDetailsHandler.php | 4 + ...eckoutPaymentCommentHistoryHandlerTest.php | 122 ------------------ etc/di.xml | 1 - 4 files changed, 4 insertions(+), 170 deletions(-) delete mode 100644 Gateway/Response/CheckoutPaymentCommentHistoryHandler.php delete mode 100644 Test/Unit/Gateway/Response/CheckoutPaymentCommentHistoryHandlerTest.php diff --git a/Gateway/Response/CheckoutPaymentCommentHistoryHandler.php b/Gateway/Response/CheckoutPaymentCommentHistoryHandler.php deleted file mode 100644 index 1fc333259..000000000 --- a/Gateway/Response/CheckoutPaymentCommentHistoryHandler.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Magento\Payment\Gateway\Helper\SubjectReader; -use Magento\Payment\Gateway\Response\HandlerInterface; - -class CheckoutPaymentCommentHistoryHandler implements HandlerInterface -{ - /** - * @param array $handlingSubject - * @param array $responseCollection - * @return $this - */ - public function handle(array $handlingSubject, array $responseCollection): self - { - $readPayment = SubjectReader::readPayment($handlingSubject); - $payment = $readPayment->getPayment(); - $comment = __("Adyen Result response:"); - - foreach ($responseCollection as $response) { - $responseCode = $response['resultCode'] ?? $response['response'] ?? ''; - - if (!empty($responseCode)) { - $comment .= '
' . __('authResult:') . ' ' . $responseCode; - $payment->getOrder()->setAdyenResulturlEventCode($responseCode); - } - - if (isset($response['pspReference'])) { - $comment .= '
' . __('pspReference:') . ' ' . $response['pspReference']; - } - $comment .= '
'; - } - - $payment->getOrder()->addStatusHistoryComment($comment, $payment->getOrder()->getStatus()); - return $this; - } -} diff --git a/Gateway/Response/CheckoutPaymentsDetailsHandler.php b/Gateway/Response/CheckoutPaymentsDetailsHandler.php index 1731b7db1..495bd480d 100644 --- a/Gateway/Response/CheckoutPaymentsDetailsHandler.php +++ b/Gateway/Response/CheckoutPaymentsDetailsHandler.php @@ -69,6 +69,10 @@ public function handle(array $handlingSubject, array $responseCollection) $payment->setTransactionId($response['pspReference']); } + if (isset($response['resultCode'])) { + $payment->getOrder()->setAdyenResulturlEventCode($response['resultCode']); + } + // do not close transaction, so you can do a cancel() and void $payment->setIsTransactionClosed(false); $payment->setShouldCloseParentTransaction(false); diff --git a/Test/Unit/Gateway/Response/CheckoutPaymentCommentHistoryHandlerTest.php b/Test/Unit/Gateway/Response/CheckoutPaymentCommentHistoryHandlerTest.php deleted file mode 100644 index fc6f90b54..000000000 --- a/Test/Unit/Gateway/Response/CheckoutPaymentCommentHistoryHandlerTest.php +++ /dev/null @@ -1,122 +0,0 @@ -checkoutPaymentCommentHistoryHandler = new CheckoutPaymentCommentHistoryHandler(); - - $orderAdapterMock = $this->createMock(OrderAdapterInterface::class); - $this->orderMock = $this->createMock(Order::class); - - $this->paymentMock = $this->createMock(Payment::class); - $this->paymentMock->method('getOrder')->willReturn($this->orderMock); - $this->paymentDataObject = new PaymentDataObject($orderAdapterMock, $this->paymentMock); - - $this->handlingSubject = [ - 'payment' => $this->paymentDataObject, - 'paymentAction' => "authorize", - 'stateObject' => null - ]; - } - public function testIfGeneralFlowIsHandledCorrectly() - { - // Prepare the sample response collection - $responseCollection = [ - 'hasOnlyGiftCards' => false, - 0 => [ - 'additionalData' => [], - 'amount' => [], - 'resultCode' => 'Authorised', - 'pspReference' => 'MDH54321', - 'paymentMethod' => [ - 'name' => 'giftcard', - 'type' => 'Givex', - ], - ], - 1 => [ - 'additionalData' => [], - 'amount' => [], - 'resultCode' => 'Authorised', - 'pspReference' => 'ABC12345', - 'paymentMethod' => [ - 'name' => 'card', - 'type' => 'CreditCard', - ], - ], - ]; - - // Set expectations for the mocked order object - $this->orderMock - ->expects($this->once()) - ->method('addStatusHistoryComment') - ->with( - $this->stringContains( - "authResult: Authorised
pspReference: MDH54321
" . - "
authResult: Authorised
pspReference: ABC12345
" - ), - $this->anything() - ); - - $this->checkoutPaymentCommentHistoryHandler->handle($this->handlingSubject, $responseCollection); - } - - public function testIfEmptyResponseCodeIsHandledCorrectly() - { - // Prepare a sample response collection without a resultCode - $responseCollection = [ - [ - 'pspReference' => '123456789' - ] - ]; - - // Set expectations for the mocked order object - $this->orderMock - ->expects($this->once()) - ->method('addStatusHistoryComment') - ->with( - $this->stringContains('pspReference: 123456789'), - $this->anything() - ); - - // Execute the handler - $this->checkoutPaymentCommentHistoryHandler->handle($this->handlingSubject, $responseCollection); - } - - public function testIfNoPspReferenceIsHandledCorrectly() - { - // Prepare a sample response collection without a pspReference - $responseCollection = [ - [ - 'resultCode' => 'Authorised' - ] - ]; - - // Set expectations for the mocked order object - $this->orderMock - ->expects($this->once()) - ->method('addStatusHistoryComment') - ->with( - $this->stringContains('authResult: Authorised'), - $this->anything() - ); - - // Execute the handler - $this->checkoutPaymentCommentHistoryHandler->handle($this->handlingSubject, $responseCollection); - } -} diff --git a/etc/di.xml b/etc/di.xml index 6d52bc461..357694bbe 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1364,7 +1364,6 @@ Adyen\Payment\Gateway\Response\CheckoutPaymentsDetailsHandler Adyen\Payment\Gateway\Response\VaultDetailsHandler - Adyen\Payment\Gateway\Response\CheckoutPaymentCommentHistoryHandler From b6543fba9185f58354bd26a9c0e077903d579b5d Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 10:21:23 +0100 Subject: [PATCH 09/42] [ECP-9639] Refactor CheckoutResponseValidator --- .../Validator/CheckoutResponseValidator.php | 139 ++++-------------- etc/di.xml | 22 +-- 2 files changed, 40 insertions(+), 121 deletions(-) diff --git a/Gateway/Validator/CheckoutResponseValidator.php b/Gateway/Validator/CheckoutResponseValidator.php index b2e59bbc5..cb2a413e8 100644 --- a/Gateway/Validator/CheckoutResponseValidator.php +++ b/Gateway/Validator/CheckoutResponseValidator.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2015 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -11,71 +11,56 @@ namespace Adyen\Payment\Gateway\Validator; -use Adyen\Payment\Helper\Data; +use Adyen\Model\Checkout\PaymentResponse; +use Adyen\Payment\Exception\AbstractAdyenException; use Adyen\Payment\Logger\AdyenLogger; use Magento\Framework\Exception\ValidatorException; -use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Validator\AbstractValidator; use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; class CheckoutResponseValidator extends AbstractValidator { - /** - * @var AdyenLogger - */ - private $adyenLogger; - - /** - * @var Data - */ - private $adyenHelper; - - /** - * @var Array - */ - const ALLOWED_ERROR_CODES = ['124']; + const VALID_RESULT_CODES = [ + PaymentResponse::RESULT_CODE_AUTHORISED, + PaymentResponse::RESULT_CODE_RECEIVED, + PaymentResponse::RESULT_CODE_IDENTIFY_SHOPPER, + PaymentResponse::RESULT_CODE_CHALLENGE_SHOPPER, + PaymentResponse::RESULT_CODE_PRESENT_TO_SHOPPER, + PaymentResponse::RESULT_CODE_PENDING, + PaymentResponse::RESULT_CODE_REDIRECT_SHOPPER + ]; /** - * CheckoutResponseValidator constructor. - * * @param ResultInterfaceFactory $resultFactory * @param AdyenLogger $adyenLogger - * @param Data $adyenHelper */ public function __construct( ResultInterfaceFactory $resultFactory, - AdyenLogger $adyenLogger, - Data $adyenHelper + private readonly AdyenLogger $adyenLogger ) { - $this->adyenLogger = $adyenLogger; - $this->adyenHelper = $adyenHelper; parent::__construct($resultFactory); } /** * @param array $validationSubject * @return ResultInterface + * @throws ValidatorException */ public function validate(array $validationSubject): ResultInterface { - // Extract all the payment responses $responseCollection = $validationSubject['response']; - unset($validationSubject['response']); - - // hasOnlyGiftCards is needed later but cannot be processed as a response - unset($responseCollection['hasOnlyGiftCards']); - - // Assign the remaining items to $commandSubject - $commandSubject = $validationSubject; if (empty($responseCollection)) { throw new ValidatorException(__("No responses were provided")); } - foreach ($responseCollection as $thisResponse) { - $responseSubject = array_merge($commandSubject, ['response' => $thisResponse]); - $this->validateResponse($responseSubject); + foreach ($responseCollection as $response) { + if (empty($response['resultCode'])) { + $this->handleEmptyResultCode($response); + } else { + $this->validateResultCode($response['resultCode']); + } } return $this->createResult(true); @@ -84,90 +69,30 @@ public function validate(array $validationSubject): ResultInterface /** * @throws ValidatorException */ - private function validateResponse($responseSubject): void - { - $response = SubjectReader::readResponse($responseSubject); - $paymentDataObjectInterface = SubjectReader::readPayment($responseSubject); - $payment = $paymentDataObjectInterface->getPayment(); - - $payment->setAdditionalInformation('3dActive', false); - - // Handle empty result for unexpected cases - if (empty($response['resultCode'])) { - $this->handleEmptyResultCode($response); - } - - // Handle the `/payments` response - $this->validateResult($response, $payment); - } - - /** - * @throws ValidatorException - */ - private function validateResult($response, $payment) + private function validateResultCode(string $resultCode): void { - $resultCode = $response['resultCode']; - $payment->setAdditionalInformation('resultCode', $resultCode); - - if (!empty($response['action'])) { - $payment->setAdditionalInformation('action', $response['action']); - } - - if (!empty($response['additionalData'])) { - $payment->setAdditionalInformation('additionalData', $response['additionalData']); - } - - if (!empty($response['pspReference'])) { - $payment->setAdditionalInformation('pspReference', $response['pspReference']); - } - - if (!empty($response['details'])) { - $payment->setAdditionalInformation('details', $response['details']); - } - - if (!empty($response['donationToken'])) { - $payment->setAdditionalInformation('donationToken', $response['donationToken']); - } - - switch ($resultCode) { - case "Authorised": - case "Received": - // Save cc_type if available in the response - if (!empty($response['additionalData']['paymentMethod'])) { - $ccType = $this->adyenHelper->getMagentoCreditCartType( - $response['additionalData']['paymentMethod'] - ); - $payment->setAdditionalInformation('cc_type', $ccType); - $payment->setCcType($ccType); - } - break; - case "IdentifyShopper": - case "ChallengeShopper": - case "PresentToShopper": - case 'Pending': - case "RedirectShopper": - // nothing extra - break; - case "Refused": - $errorMsg = __('The payment is REFUSED.'); - // this will result the specific error - throw new ValidatorException($errorMsg); - default: - $errorMsg = __('Error with payment method please select different payment method.'); - throw new ValidatorException($errorMsg); + if (strcmp($resultCode, PaymentResponse::RESULT_CODE_REFUSED) === 0) { + $errorMsg = __('The payment is REFUSED.'); + // this will result the specific error + throw new ValidatorException($errorMsg); + } elseif (!in_array($resultCode, self::VALID_RESULT_CODES, true)) { + $errorMsg = __('Error with payment method please select different payment method.'); + throw new ValidatorException($errorMsg); } } /** * @throws ValidatorException */ - private function handleEmptyResultCode($response): never + private function handleEmptyResultCode($response): void { if (!empty($response['error'])) { $this->adyenLogger->error($response['error']); } - if (!empty($response['errorCode']) && !empty($response['error']) && in_array($response['errorCode'], self::ALLOWED_ERROR_CODES, true)) { + if (!empty($response['errorCode']) && + !empty($response['error']) && + in_array($response['errorCode'], AbstractAdyenException::SAFE_ERROR_CODES, true)) { $errorMsg = __($response['error']); } else { $errorMsg = __('Error with payment method, please select a different payment method.'); diff --git a/etc/di.xml b/etc/di.xml index 357694bbe..908d5efdc 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -846,7 +846,7 @@ AdyenPaymentCcVaultAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentResponseHandlerComposite @@ -886,7 +886,7 @@ AdyenPaymentVaultAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentVaultResponseHandlerComposite @@ -938,7 +938,7 @@ AdyenPaymentCcAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentResponseHandlerComposite @@ -947,7 +947,7 @@ AdyenPaymentMotoAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentResponseHandlerComposite @@ -956,7 +956,7 @@ AdyenPaymentOneclickAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentResponseHandlerComposite @@ -965,7 +965,7 @@ AdyenPaymentAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentResponseHandlerComposite @@ -974,7 +974,7 @@ AdyenPaymentGiftcardAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentResponseHandlerComposite @@ -983,7 +983,7 @@ AdyenPaymentBoletoAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment - CheckoutResponseValidator + Adyen\Payment\Gateway\Validator\CheckoutResponseValidator AdyenPaymentBoletoResponseHandlerComposite @@ -1476,12 +1476,6 @@ - - - - Adyen\Payment\Logger\AdyenLogger - - From 93b252a8a206e6047110f4eeb7f90ffa9881049e Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 10:30:35 +0100 Subject: [PATCH 10/42] [ECP-9639] Add argument type declaration --- Gateway/Validator/CheckoutResponseValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gateway/Validator/CheckoutResponseValidator.php b/Gateway/Validator/CheckoutResponseValidator.php index cb2a413e8..337370866 100644 --- a/Gateway/Validator/CheckoutResponseValidator.php +++ b/Gateway/Validator/CheckoutResponseValidator.php @@ -84,7 +84,7 @@ private function validateResultCode(string $resultCode): void /** * @throws ValidatorException */ - private function handleEmptyResultCode($response): void + private function handleEmptyResultCode(array $response): void { if (!empty($response['error'])) { $this->adyenLogger->error($response['error']); From c19260a130ccbeef76a5939778746220702286bb Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 10:32:01 +0100 Subject: [PATCH 11/42] [ECP-9639] Use SubjectReader for response collection --- Gateway/Validator/CheckoutResponseValidator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gateway/Validator/CheckoutResponseValidator.php b/Gateway/Validator/CheckoutResponseValidator.php index 337370866..b07d4d4ab 100644 --- a/Gateway/Validator/CheckoutResponseValidator.php +++ b/Gateway/Validator/CheckoutResponseValidator.php @@ -15,6 +15,7 @@ use Adyen\Payment\Exception\AbstractAdyenException; use Adyen\Payment\Logger\AdyenLogger; use Magento\Framework\Exception\ValidatorException; +use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Validator\AbstractValidator; use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; @@ -49,7 +50,7 @@ public function __construct( */ public function validate(array $validationSubject): ResultInterface { - $responseCollection = $validationSubject['response']; + $responseCollection = SubjectReader::readResponse($validationSubject); if (empty($responseCollection)) { throw new ValidatorException(__("No responses were provided")); From 13a9b324729e51ca06eff815becc6db353d4dbc4 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 11:27:32 +0100 Subject: [PATCH 12/42] [ECP-9639] Implement abstract response validator for modification requests --- .../AbstractModificationResponseValidator.php | 94 ++++++++++++++++ Gateway/Validator/CancelResponseValidator.php | 65 ----------- .../Validator/CaptureResponseValidator.php | 102 ------------------ Gateway/Validator/RefundResponseValidator.php | 64 ----------- etc/di.xml | 29 +++-- 5 files changed, 117 insertions(+), 237 deletions(-) create mode 100644 Gateway/Validator/AbstractModificationResponseValidator.php delete mode 100644 Gateway/Validator/CancelResponseValidator.php delete mode 100644 Gateway/Validator/CaptureResponseValidator.php delete mode 100644 Gateway/Validator/RefundResponseValidator.php diff --git a/Gateway/Validator/AbstractModificationResponseValidator.php b/Gateway/Validator/AbstractModificationResponseValidator.php new file mode 100644 index 000000000..7fb3802a0 --- /dev/null +++ b/Gateway/Validator/AbstractModificationResponseValidator.php @@ -0,0 +1,94 @@ + + */ + +namespace Adyen\Payment\Gateway\Validator; + +use Adyen\Model\Checkout\PaymentCancelResponse; +use Adyen\Model\Checkout\PaymentCaptureResponse; +use Adyen\Model\Checkout\PaymentRefundResponse; +use Adyen\Payment\Logger\AdyenLogger; +use Magento\Framework\Exception\ValidatorException; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Validator\AbstractValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; + +class AbstractModificationResponseValidator extends AbstractValidator +{ + const VALID_STATUSES = [ + PaymentCaptureResponse::STATUS_RECEIVED, + PaymentRefundResponse::STATUS_RECEIVED, + PaymentCancelResponse::STATUS_RECEIVED + ]; + + /** + * @param ResultInterfaceFactory $resultFactory + * @param AdyenLogger $adyenLogger + * @param string $modificationType + */ + public function __construct( + ResultInterfaceFactory $resultFactory, + private readonly AdyenLogger $adyenLogger, + private readonly string $modificationType + ) { + parent::__construct($resultFactory); + } + + /** + * @param array $validationSubject + * @return ResultInterface + * @throws ValidatorException + */ + public function validate(array $validationSubject): ResultInterface + { + $responseCollection = SubjectReader::readResponse($validationSubject); + + if (empty($responseCollection)) { + throw new ValidatorException(__("No responses were provided")); + } + + $isValid = true; + $errorMessages = []; + + foreach ($responseCollection as $response) { + if (empty($response['status']) || !in_array($response['status'], self::VALID_STATUSES)) { + $errorMessage = __( + 'An error occurred while validating the %1 response', + $this->getModificationType() + ); + + $logMessage = sprintf( + "An error occurred while validating the %s response%s. %s", + $this->getModificationType(), + isset($response['formattedModificationAmount']) ? + ' with amount ' . + $response['formattedModificationAmount'] : '', + $response['error'] ?? '' + ); + + $this->adyenLogger->error($logMessage); + + $errorMessages[] = $errorMessage; + $isValid = false; + } + } + + return $this->createResult($isValid, $errorMessages); + } + + /** + * @return string + */ + private function getModificationType(): string + { + return $this->modificationType; + } +} diff --git a/Gateway/Validator/CancelResponseValidator.php b/Gateway/Validator/CancelResponseValidator.php deleted file mode 100644 index 83840bc34..000000000 --- a/Gateway/Validator/CancelResponseValidator.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Validator; - -use Magento\Payment\Gateway\Validator\AbstractValidator; - -class CancelResponseValidator extends AbstractValidator -{ - /** - * @var \Adyen\Payment\Logger\AdyenLogger - */ - private $adyenLogger; - - /** - * CancelResponseValidator constructor. - * - * @param \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory - * @param \Adyen\Payment\Logger\AdyenLogger $adyenLogger - */ - public function __construct( - \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory, - \Adyen\Payment\Logger\AdyenLogger $adyenLogger - ) { - $this->adyenLogger = $adyenLogger; - parent::__construct($resultFactory); - } - - /** - * @param array $validationSubject - * @return \Magento\Payment\Gateway\Validator\ResultInterface - */ - public function validate(array $validationSubject) - { - $response = \Magento\Payment\Gateway\Helper\SubjectReader::readResponse($validationSubject); - - $isValid = true; - $errorMessages = []; - - // The available response code that the API can return in case of a successful cancellation - $expectedResponses = ['received']; - - if (empty($response['status']) || !in_array($response['status'], $expectedResponses)) { - $errorMsg = __('Error with cancellation'); - $this->adyenLogger->error($errorMsg); - - if (!empty($response['error'])) { - $this->adyenLogger->error($response['error']); - } - - $errorMessages[] = $errorMsg; - $isValid = false; - } - - return $this->createResult($isValid, $errorMessages); - } -} diff --git a/Gateway/Validator/CaptureResponseValidator.php b/Gateway/Validator/CaptureResponseValidator.php deleted file mode 100644 index dff6986bb..000000000 --- a/Gateway/Validator/CaptureResponseValidator.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Validator; - -use Adyen\Payment\Gateway\Http\Client\TransactionCapture; -use Magento\Payment\Gateway\Validator\AbstractValidator; - -class CaptureResponseValidator extends AbstractValidator -{ - /** - * @var \Adyen\Payment\Logger\AdyenLogger - */ - private $adyenLogger; - - /** - * CaptureResponseValidator constructor. - * - * @param \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory - * @param \Adyen\Payment\Logger\AdyenLogger $adyenLogger - */ - public function __construct( - \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory, - \Adyen\Payment\Logger\AdyenLogger $adyenLogger - ) { - $this->adyenLogger = $adyenLogger; - parent::__construct($resultFactory); - } - - /** - * @param array $validationSubject - * @return \Magento\Payment\Gateway\Validator\ResultInterface - */ - public function validate(array $validationSubject) - { - $response = \Magento\Payment\Gateway\Helper\SubjectReader::readResponse($validationSubject); - - $isValid = true; - $errorMessages = []; - - if (array_key_exists(TransactionCapture::MULTIPLE_AUTHORIZATIONS, $response)) { - return $this->validateMultipleCaptureRequests($response); - } - - if (empty($response['status']) || $response['status'] != TransactionCapture::CAPTURE_RECEIVED) { - $errorMessages[] = $this->buildErrorMessages($response); - $isValid = false; - } - - return $this->createResult($isValid, $errorMessages); - } - - /** - * Validate a response which contains multiple capture responses - * - * @param $responseContainer - * @return \Magento\Payment\Gateway\Validator\ResultInterface - */ - public function validateMultipleCaptureRequests($responseContainer) - { - $isValid = true; - $errorMessages = []; - - foreach ($responseContainer[TransactionCapture::MULTIPLE_AUTHORIZATIONS] as $response) { - if (empty($response['status']) || $response['status'] != TransactionCapture::CAPTURE_RECEIVED) { - $errorMessages[] = $this->buildErrorMessages($response, true); - } - } - - return $this->createResult($isValid, $errorMessages); - } - - /** - * @param $response - * @param bool $multiple - * @return \Magento\Framework\Phrase|string - */ - private function buildErrorMessages($response, bool $multiple = false) - { - if ($multiple && array_key_exists(TransactionCapture::FORMATTED_CAPTURE_AMOUNT, $response)) { - $errorMsg = __('Error with capture on transaction with amount') . $response[TransactionCapture::FORMATTED_CAPTURE_AMOUNT]; - } else { - $errorMsg = __('Error with capture'); - } - - $this->adyenLogger->error($errorMsg); - - if (!empty($response['error'])) { - $this->adyenLogger->error($response['error']); - } - - return $errorMsg; - } -} diff --git a/Gateway/Validator/RefundResponseValidator.php b/Gateway/Validator/RefundResponseValidator.php deleted file mode 100644 index 47c8745c5..000000000 --- a/Gateway/Validator/RefundResponseValidator.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Validator; - -use Magento\Payment\Gateway\Validator\AbstractValidator; - -class RefundResponseValidator extends AbstractValidator -{ - /** - * @var \Adyen\Payment\Logger\AdyenLogger - */ - private $adyenLogger; - - /** - * RefundResponseValidator constructor. - * - * @param \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory - * @param \Adyen\Payment\Logger\AdyenLogger $adyenLogger - */ - public function __construct( - \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory, - \Adyen\Payment\Logger\AdyenLogger $adyenLogger - ) { - $this->adyenLogger = $adyenLogger; - parent::__construct($resultFactory); - } - - /** - * @param array $validationSubject - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function validate(array $validationSubject) - { - $responses = \Magento\Payment\Gateway\Helper\SubjectReader::readResponse($validationSubject); - - $isValid = true; - $errorMessages = []; - - foreach ($responses as $response) { - if (empty($response['status']) || $response['status'] != 'received') { - $errorMsg = __('Error with refund'); - $this->adyenLogger->error($errorMsg); - - if (!empty($response['error'])) { - $this->adyenLogger->error($response['error']); - } - - $errorMessages[] = $errorMsg; - $isValid = false; - } - } - - return $this->createResult($isValid, $errorMessages); - } -} diff --git a/etc/di.xml b/etc/di.xml index 908d5efdc..b60fd7b78 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1002,7 +1002,7 @@ AdyenPaymentCaptureRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionCapture - Adyen\Payment\Gateway\Validator\CaptureResponseValidator + AdyenPaymentCaptureResponseValidator AdyenPaymentCaptureResponseHandlerComposite @@ -1011,7 +1011,7 @@ AdyenPaymentMotoCaptureRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionCapture - Adyen\Payment\Gateway\Validator\CaptureResponseValidator + AdyenPaymentCaptureResponseValidator AdyenPaymentCaptureResponseHandlerComposite @@ -1021,7 +1021,7 @@ AdyenPaymentRefundRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionRefund - Adyen\Payment\Gateway\Validator\RefundResponseValidator + AdyenPaymentRefundResponseValidator AdyenPaymentRefundResponseHandlerComposite @@ -1030,7 +1030,7 @@ AdyenPaymentMotoRefundRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionRefund - Adyen\Payment\Gateway\Validator\RefundResponseValidator + AdyenPaymentRefundResponseValidator AdyenPaymentRefundResponseHandlerComposite @@ -1039,7 +1039,7 @@ AdyenPaymentCancelRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionCancel - Adyen\Payment\Gateway\Validator\CancelResponseValidator + AdyenPaymentCancelResponseValidator AdyenPaymentCancelResponseHandlerComposite @@ -1048,10 +1048,27 @@ AdyenPaymentMotoCancelRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionCancel - Adyen\Payment\Gateway\Validator\CancelResponseValidator + AdyenPaymentCancelResponseValidator AdyenPaymentCancelResponseHandlerComposite + + + + capture + + + + + refund + + + + + cancel + + + From c793c2b471724afd4305d7bab88e9572ffeb96db Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 12:00:39 +0100 Subject: [PATCH 13/42] [ECP-9639] Move abstract class parameters to di.xml --- .../AbstractModificationResponseValidator.php | 25 ++++++++++--------- etc/di.xml | 9 +++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Gateway/Validator/AbstractModificationResponseValidator.php b/Gateway/Validator/AbstractModificationResponseValidator.php index 7fb3802a0..db887fd3e 100644 --- a/Gateway/Validator/AbstractModificationResponseValidator.php +++ b/Gateway/Validator/AbstractModificationResponseValidator.php @@ -11,9 +11,6 @@ namespace Adyen\Payment\Gateway\Validator; -use Adyen\Model\Checkout\PaymentCancelResponse; -use Adyen\Model\Checkout\PaymentCaptureResponse; -use Adyen\Model\Checkout\PaymentRefundResponse; use Adyen\Payment\Logger\AdyenLogger; use Magento\Framework\Exception\ValidatorException; use Magento\Payment\Gateway\Helper\SubjectReader; @@ -21,23 +18,19 @@ use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; -class AbstractModificationResponseValidator extends AbstractValidator +abstract class AbstractModificationResponseValidator extends AbstractValidator { - const VALID_STATUSES = [ - PaymentCaptureResponse::STATUS_RECEIVED, - PaymentRefundResponse::STATUS_RECEIVED, - PaymentCancelResponse::STATUS_RECEIVED - ]; - /** * @param ResultInterfaceFactory $resultFactory * @param AdyenLogger $adyenLogger * @param string $modificationType + * @param array $validStatuses */ public function __construct( ResultInterfaceFactory $resultFactory, private readonly AdyenLogger $adyenLogger, - private readonly string $modificationType + private readonly string $modificationType, + private readonly array $validStatuses ) { parent::__construct($resultFactory); } @@ -59,7 +52,7 @@ public function validate(array $validationSubject): ResultInterface $errorMessages = []; foreach ($responseCollection as $response) { - if (empty($response['status']) || !in_array($response['status'], self::VALID_STATUSES)) { + if (empty($response['status']) || !in_array($response['status'], $this->getValidStatuses())) { $errorMessage = __( 'An error occurred while validating the %1 response', $this->getModificationType() @@ -91,4 +84,12 @@ private function getModificationType(): string { return $this->modificationType; } + + /** + * @return array + */ + private function getValidStatuses(): array + { + return $this->validStatuses; + } } diff --git a/etc/di.xml b/etc/di.xml index b60fd7b78..523240e44 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1056,16 +1056,25 @@ capture + + Adyen\Model\Checkout\PaymentCaptureResponse\PaymentCaptureResponse::STATUS_RECEIVED + refund + + Adyen\Model\Checkout\PaymentRefundResponse\PaymentRefundResponse::STATUS_RECEIVED + cancel + + Adyen\Model\Checkout\PaymentCancelResponse\PaymentCancelResponse::STATUS_RECEIVED + From 39468118ae5111619bb8161fd4ef77630d494cff Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 12:08:41 +0100 Subject: [PATCH 14/42] [ECP-9639] Refactor Pay by Link request validator --- Gateway/Validator/PayByLinkValidator.php | 40 ----------------- .../PaymentLinksRequestValidator.php | 45 +++++++++++++++++++ ...p => PaymentLinksRequestValidatorTest.php} | 8 ++-- etc/di.xml | 2 +- 4 files changed, 50 insertions(+), 45 deletions(-) delete mode 100644 Gateway/Validator/PayByLinkValidator.php create mode 100644 Gateway/Validator/PaymentLinksRequestValidator.php rename Test/Unit/Gateway/Validator/{PayByLinkValidatorTest.php => PaymentLinksRequestValidatorTest.php} (92%) diff --git a/Gateway/Validator/PayByLinkValidator.php b/Gateway/Validator/PayByLinkValidator.php deleted file mode 100644 index f37b52870..000000000 --- a/Gateway/Validator/PayByLinkValidator.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Validator; - -use Adyen\Payment\Model\Ui\AdyenPayByLinkConfigProvider; -use Magento\Payment\Gateway\Validator\AbstractValidator; - -class PayByLinkValidator extends AbstractValidator -{ - /** - * @inheritdoc - */ - public function validate(array $validationSubject) - { - $payment = $validationSubject['payment']; - $expiresAt = $payment->getAdyenPblExpiresAt() . ' 23:59:59'; - if (!is_null($expiresAt) && $expiryDate = date_create_from_format( - AdyenPayByLinkConfigProvider::DATE_TIME_FORMAT, - $expiresAt - )) { - $daysToExpire = ($expiryDate->getTimestamp() - time()) / 86400; - if ( - $daysToExpire <= AdyenPayByLinkConfigProvider::MIN_EXPIRY_DAYS || - $daysToExpire >= AdyenPayByLinkConfigProvider::MAX_EXPIRY_DAYS - ) { - return $this->createResult(false, ['Invalid expiry date selected for Adyen Pay By Link']); - } - } - return $this->createResult(true); - } -} diff --git a/Gateway/Validator/PaymentLinksRequestValidator.php b/Gateway/Validator/PaymentLinksRequestValidator.php new file mode 100644 index 000000000..d03f0b940 --- /dev/null +++ b/Gateway/Validator/PaymentLinksRequestValidator.php @@ -0,0 +1,45 @@ + + */ + +namespace Adyen\Payment\Gateway\Validator; + +use Adyen\Payment\Model\Ui\AdyenPayByLinkConfigProvider; +use Magento\Payment\Gateway\Validator\AbstractValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; + +class PaymentLinksRequestValidator extends AbstractValidator +{ + /** + * @param array $validationSubject + * @return ResultInterface + */ + public function validate(array $validationSubject): ResultInterface + { + $payment = $validationSubject['payment']; + $expiresAt = $payment->getAdyenPblExpiresAt() . ' 23:59:59'; + + $isValid = true; + $errorMessages = []; + + if ($expiryDate = date_create_from_format(AdyenPayByLinkConfigProvider::DATE_TIME_FORMAT, $expiresAt)) { + $daysToExpire = ($expiryDate->getTimestamp() - time()) / 86400; + + if ($daysToExpire <= AdyenPayByLinkConfigProvider::MIN_EXPIRY_DAYS || + $daysToExpire >= AdyenPayByLinkConfigProvider::MAX_EXPIRY_DAYS + ) { + $isValid = false; + $errorMessages[] = 'Invalid expiry date selected for Adyen Pay By Link'; + } + } + + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/Test/Unit/Gateway/Validator/PayByLinkValidatorTest.php b/Test/Unit/Gateway/Validator/PaymentLinksRequestValidatorTest.php similarity index 92% rename from Test/Unit/Gateway/Validator/PayByLinkValidatorTest.php rename to Test/Unit/Gateway/Validator/PaymentLinksRequestValidatorTest.php index e5587feae..06c5cfb2a 100755 --- a/Test/Unit/Gateway/Validator/PayByLinkValidatorTest.php +++ b/Test/Unit/Gateway/Validator/PaymentLinksRequestValidatorTest.php @@ -11,16 +11,16 @@ namespace Adyen\Payment\Test\Gateway\Validator; -use Adyen\Payment\Gateway\Validator\PayByLinkValidator; +use Adyen\Payment\Gateway\Validator\PaymentLinksRequestValidator; use Adyen\Payment\Model\Ui\AdyenPayByLinkConfigProvider; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; use Magento\Quote\Model\Quote\Payment; use PHPUnit\Framework\TestCase; -class PayByLinkValidatorTest extends TestCase +class PaymentLinksRequestValidatorTest extends TestCase { /** - * @var PayByLinkValidator + * @var PaymentLinksRequestValidator */ private $payByLinkValidator; @@ -57,7 +57,7 @@ protected function setUp(): void ] ])); - $this->payByLinkValidator = new PayByLinkValidator($resultInterfaceFactory); + $this->payByLinkValidator = new PaymentLinksRequestValidator($resultInterfaceFactory); $this->payment = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor()->setMethods(['getAdyenPblExpiresAt']) diff --git a/etc/di.xml b/etc/di.xml index 523240e44..6f03773c8 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1498,7 +1498,7 @@ - Adyen\Payment\Gateway\Validator\PayByLinkValidator + Adyen\Payment\Gateway\Validator\PaymentLinksRequestValidator From 26e32f9c7abb25d91911edc230d776540072c3ec Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 12:13:28 +0100 Subject: [PATCH 15/42] [ECP-9639] Refactor InstallmentValidator class --- ...or.php => InstallmentRequestValidator.php} | 78 ++++++------------- etc/di.xml | 8 +- 2 files changed, 27 insertions(+), 59 deletions(-) rename Gateway/Validator/{InstallmentValidator.php => InstallmentRequestValidator.php} (65%) diff --git a/Gateway/Validator/InstallmentValidator.php b/Gateway/Validator/InstallmentRequestValidator.php similarity index 65% rename from Gateway/Validator/InstallmentValidator.php rename to Gateway/Validator/InstallmentRequestValidator.php index 43d2a8c0b..6b8463c9c 100644 --- a/Gateway/Validator/InstallmentValidator.php +++ b/Gateway/Validator/InstallmentRequestValidator.php @@ -3,112 +3,79 @@ * * Adyen Payment Module * - * Copyright (c) 2017 Adyen B.V. + * Copyright (c) 2025 Adyen N.V. * This file is open source and available under the MIT license. * See the LICENSE file for more info. * * Author: Adyen */ - namespace Adyen\Payment\Gateway\Validator; use Adyen\Payment\Helper\Config; -use Adyen\Payment\Helper\CaseManagement; use Adyen\Payment\Helper\ChargedCurrency; -use Adyen\Payment\Helper\Data; -use Adyen\Payment\Logger\AdyenLogger; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Serialize\SerializerInterface; use Magento\Payment\Gateway\Validator\AbstractValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; use Magento\Quote\Model\QuoteRepository; -class InstallmentValidator extends AbstractValidator +class InstallmentRequestValidator extends AbstractValidator { /** - * @var \Adyen\Payment\Logger\AdyenLogger - */ - private $adyenLogger; - - /** - * @var \Adyen\Payment\Helper\Config - */ - private $configHelper; - - /** - * @var \Adyen\Payment\Helper\Data - */ - private $adyenHelper; - - /** - * @var \Magento\Framework\Serialize\SerializerInterface - */ - private $serializer; - - /** - * @var \Magento\Quote\Model\QuoteRepository - */ - private $quoteRepository; - - /** - * @var ChargedCurrency - */ - private $chargedCurrency; - - - /** - * InstallmentValidator constructor. - * * @param ResultInterfaceFactory $resultFactory * @param Config $configHelper - * @param AdyenLogger $adyenLogger - * @param Data $adyenHelper * @param SerializerInterface $serializer * @param QuoteRepository $quoteRepository * @param ChargedCurrency $chargedCurrency */ public function __construct( - \Magento\Payment\Gateway\Validator\ResultInterfaceFactory $resultFactory, - \Adyen\Payment\Helper\Config $configHelper, - \Adyen\Payment\Logger\AdyenLogger $adyenLogger, - \Adyen\Payment\Helper\Data $adyenHelper, - \Magento\Framework\Serialize\SerializerInterface $serializer, - \Magento\Quote\Model\QuoteRepository $quoteRepository, - ChargedCurrency $chargedCurrency + ResultInterfaceFactory $resultFactory, + private readonly Config $configHelper, + private readonly SerializerInterface $serializer, + private readonly QuoteRepository $quoteRepository, + private readonly ChargedCurrency $chargedCurrency ) { - $this->adyenLogger = $adyenLogger; - $this->adyenHelper = $adyenHelper; - $this->configHelper = $configHelper; - $this->serializer = $serializer; - $this->quoteRepository = $quoteRepository; - $this->chargedCurrency = $chargedCurrency; parent::__construct($resultFactory); } - public function validate(array $validationSubject) + /** + * @param array $validationSubject + * @return ResultInterface + * @throws NoSuchEntityException + */ + public function validate(array $validationSubject): ResultInterface { $isValid = true; $fails = []; $payment = $validationSubject['payment']; $quoteId = $payment->getQuoteId(); + //This validator also runs for other payments that don't necesarily have a quoteId if ($quoteId) { $quote = $this->quoteRepository->get($quoteId); } else { $quote = false; } + $installmentsEnabled = $this->configHelper->getAdyenCcConfigData('enable_installments'); if ($quote && $installmentsEnabled) { $grandTotal = $this->chargedCurrency->getQuoteAmountCurrency($quote)->getAmount(); + $installmentsAvailable = $this->configHelper->getAdyenCcConfigData('installments'); $installmentSelected = $payment->getAdditionalInformation('number_of_installments'); + $ccType = $payment->getAdditionalInformation('cc_type'); + if ($installmentsAvailable) { $installments = $this->serializer->unserialize($installmentsAvailable); } + if ($installmentSelected && $installmentsAvailable) { $isValid = false; $fails[] = __('Installments not valid.'); + if ($installments) { foreach ($installments as $ccTypeInstallment => $installment) { if ($ccTypeInstallment == $ccType) { @@ -126,6 +93,7 @@ public function validate(array $validationSubject) } } } + return $this->createResult($isValid, $fails); } } diff --git a/etc/di.xml b/etc/di.xml index 6f03773c8..f0b364c07 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1429,7 +1429,7 @@ AdyenCcCountryValidator - Adyen\Payment\Gateway\Validator\InstallmentValidator + Adyen\Payment\Gateway\Validator\InstallmentRequestValidator @@ -1437,7 +1437,7 @@ AdyenCcCountryValidator - Adyen\Payment\Gateway\Validator\InstallmentValidator + Adyen\Payment\Gateway\Validator\InstallmentRequestValidator @@ -1450,7 +1450,7 @@ AdyenOneclickCountryValidator - Adyen\Payment\Gateway\Validator\InstallmentValidator + Adyen\Payment\Gateway\Validator\InstallmentRequestValidator @@ -1593,7 +1593,7 @@ Magento\Framework\Serialize\Serializer\Serialize - + Magento\Framework\Serialize\Serializer\Serialize From 0e36438ce586f00df1876fe112ad1ddf785c806a Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 12:15:43 +0100 Subject: [PATCH 16/42] [ECP-9639] Remove hasOnlyGiftCards temporary handler --- Gateway/Response/AbstractOrderStatusHistoryHandler.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Gateway/Response/AbstractOrderStatusHistoryHandler.php b/Gateway/Response/AbstractOrderStatusHistoryHandler.php index 536c187a9..f0c94c543 100644 --- a/Gateway/Response/AbstractOrderStatusHistoryHandler.php +++ b/Gateway/Response/AbstractOrderStatusHistoryHandler.php @@ -42,11 +42,6 @@ public function handle(array $handlingSubject, array $responseCollection): void $payment = $readPayment->getPayment(); $order = $payment->getOrder(); - // Temporary workaround to clean-up `hasOnlyGiftCards` key. It needs to be handled separately. - if (isset($responseCollection['hasOnlyGiftCards'])) { - unset($responseCollection['hasOnlyGiftCards']); - } - foreach ($responseCollection as $response) { $comment = $this->orderStatusHistoryHelper->buildApiResponseComment($response, $this->eventType); $order->addCommentToStatusHistory($comment, $order->getStatus()); From f681b9c7e9cdfb8c2defd7c6ba130adf224da9da Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 12:48:01 +0100 Subject: [PATCH 17/42] [ECP-9639] Refactor CheckoutPaymentsDetailsHandler --- .../CheckoutPaymentsDetailsHandler.php | 80 ----------------- .../CheckoutPaymentsResponseHandler.php | 87 +++++++++++++++++++ Helper/PaymentMethods.php | 1 + ...> CheckoutPaymentsResponseHandlerTest.php} | 11 +-- etc/di.xml | 4 +- 5 files changed, 96 insertions(+), 87 deletions(-) delete mode 100644 Gateway/Response/CheckoutPaymentsDetailsHandler.php create mode 100644 Gateway/Response/CheckoutPaymentsResponseHandler.php rename Test/Unit/Gateway/Response/{CheckoutPaymentsDetailsHandlerTest.php => CheckoutPaymentsResponseHandlerTest.php} (93%) diff --git a/Gateway/Response/CheckoutPaymentsDetailsHandler.php b/Gateway/Response/CheckoutPaymentsDetailsHandler.php deleted file mode 100644 index 495bd480d..000000000 --- a/Gateway/Response/CheckoutPaymentsDetailsHandler.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Adyen\Payment\Helper\Data; -use Magento\Payment\Gateway\Helper\SubjectReader; -use Magento\Payment\Gateway\Response\HandlerInterface; -use Magento\Sales\Model\Order\Payment; - -class CheckoutPaymentsDetailsHandler implements HandlerInterface -{ - /** @var Data */ - protected $adyenHelper; - const ADYEN_BOLETO = 'adyen_boleto'; - - public function __construct( - Data $adyenHelper - ) { - $this->adyenHelper = $adyenHelper; - } - - /** - * This is being used for all checkout methods (adyen hpp payment method) - * - */ - public function handle(array $handlingSubject, array $responseCollection) - { - $paymentDataObject = SubjectReader::readPayment($handlingSubject); - - /** @var Payment $payment */ - $payment = $paymentDataObject->getPayment(); - - // set transaction not to processing by default wait for notification - $payment->setIsTransactionPending(true); - - // Email sending is set at CheckoutDataBuilder for Boleto - // Otherwise, we don't want to send a confirmation email - if ($payment->getMethod() != self::ADYEN_BOLETO) { - $payment->getOrder()->setCanSendNewEmailFlag(false); - } - - // for partial payments, non-giftcard payments will always be the last element in the collection - // for non-partial, there is only one response in the collection - $response = end($responseCollection); - if (!empty($response['pspReference'])) { - // set pspReference as transactionId - $payment->setCcTransId($response['pspReference']); - $payment->setLastTransId($response['pspReference']); - - //set CC Type - $ccType = $payment->getAdditionalInformation('cc_type'); - - if (!empty($response['additionalData']['paymentMethod']) && $ccType == null) { - $ccType = $response['additionalData']['paymentMethod']; - $payment->setAdditionalInformation('cc_type', $ccType); - $payment->setCcType($ccType); - } - - // set transaction - $payment->setTransactionId($response['pspReference']); - } - - if (isset($response['resultCode'])) { - $payment->getOrder()->setAdyenResulturlEventCode($response['resultCode']); - } - - // do not close transaction, so you can do a cancel() and void - $payment->setIsTransactionClosed(false); - $payment->setShouldCloseParentTransaction(false); - } -} diff --git a/Gateway/Response/CheckoutPaymentsResponseHandler.php b/Gateway/Response/CheckoutPaymentsResponseHandler.php new file mode 100644 index 000000000..04786fc8a --- /dev/null +++ b/Gateway/Response/CheckoutPaymentsResponseHandler.php @@ -0,0 +1,87 @@ + + */ + +namespace Adyen\Payment\Gateway\Response; + +use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\PaymentMethods; +use Magento\Framework\Exception\LocalizedException; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +class CheckoutPaymentsResponseHandler implements HandlerInterface +{ + public function __construct( + protected readonly Data $adyenHelper + ) { } + + /** + * @param array $handlingSubject + * @param array $responseCollection + * @return void + * @throws LocalizedException + */ + public function handle(array $handlingSubject, array $responseCollection): void + { + // Always get the last item in the response collection and ignore the partial payments + $response = end($responseCollection); + + $paymentDataObject = SubjectReader::readPayment($handlingSubject); + /** @var Payment $payment */ + $payment = $paymentDataObject->getPayment(); + + $payment->getOrder()->setAdyenResulturlEventCode($response['resultCode']); + $payment->setAdditionalInformation('resultCode', $response['resultCode']); + $payment->setAdditionalInformation('3dActive', false); + + // Set the transaction not to processing by default wait for notification + $payment->setIsTransactionPending(true); + // Do not close the transaction, so you can do a cancel() and void + $payment->setIsTransactionClosed(false); + // Do not close the parent transaction + $payment->setShouldCloseParentTransaction(false); + + if (!empty($response['pspReference'])) { + $payment->setCcTransId($response['pspReference']); + $payment->setLastTransId($response['pspReference']); + $payment->setTransactionId($response['pspReference']); + $payment->setAdditionalInformation('pspReference', $response['pspReference']); + } + + if ($payment->getMethod() === PaymentMethods::ADYEN_CC && + !empty($response['additionalData']['paymentMethod']) && + $payment->getCcType() == null) { + $payment->setCcType($response['additionalData']['paymentMethod']); + } + + if (!empty($response['action'])) { + $payment->setAdditionalInformation('action', $response['action']); + } + + if (!empty($response['additionalData'])) { + $payment->setAdditionalInformation('additionalData', $response['additionalData']); + } + + if (!empty($response['details'])) { + $payment->setAdditionalInformation('details', $response['details']); + } + + if (!empty($response['donationToken'])) { + $payment->setAdditionalInformation('donationToken', $response['donationToken']); + } + + // Do not send order confirmation email for Boleto payments + if ($payment->getMethod() != PaymentMethods::ADYEN_BOLETO) { + $payment->getOrder()->setCanSendNewEmailFlag(false); + } + } +} diff --git a/Helper/PaymentMethods.php b/Helper/PaymentMethods.php index ee1f0f283..880721092 100644 --- a/Helper/PaymentMethods.php +++ b/Helper/PaymentMethods.php @@ -54,6 +54,7 @@ class PaymentMethods extends AbstractHelper const ADYEN_ONE_CLICK = 'adyen_oneclick'; const ADYEN_PAY_BY_LINK = 'adyen_pay_by_link'; const ADYEN_PAYPAL = 'adyen_paypal'; + const ADYEN_BOLETO = 'adyen_boleto'; const ADYEN_PREFIX = 'adyen_'; const ADYEN_CC_VAULT = 'adyen_cc_vault'; const METHODS_WITH_BRAND_LOGO = [ diff --git a/Test/Unit/Gateway/Response/CheckoutPaymentsDetailsHandlerTest.php b/Test/Unit/Gateway/Response/CheckoutPaymentsResponseHandlerTest.php similarity index 93% rename from Test/Unit/Gateway/Response/CheckoutPaymentsDetailsHandlerTest.php rename to Test/Unit/Gateway/Response/CheckoutPaymentsResponseHandlerTest.php index aa4817770..f90632c22 100644 --- a/Test/Unit/Gateway/Response/CheckoutPaymentsDetailsHandlerTest.php +++ b/Test/Unit/Gateway/Response/CheckoutPaymentsResponseHandlerTest.php @@ -2,8 +2,9 @@ namespace Test\Unit\Gateway\Response; +use Adyen\Payment\Helper\PaymentMethods; use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; -use Adyen\Payment\Gateway\Response\CheckoutPaymentsDetailsHandler; +use Adyen\Payment\Gateway\Response\CheckoutPaymentsResponseHandler; use Adyen\Payment\Helper\Data; use Magento\Payment\Gateway\Data\OrderAdapterInterface; use Magento\Payment\Gateway\Data\PaymentDataObject; @@ -11,9 +12,9 @@ use Magento\Sales\Model\Order\Payment; use PHPUnit\Framework\MockObject\MockObject; -class CheckoutPaymentsDetailsHandlerTest extends AbstractAdyenTestCase +class CheckoutPaymentsResponseHandlerTest extends AbstractAdyenTestCase { - private CheckoutPaymentsDetailsHandler $checkoutPaymentsDetailsHandler; + private CheckoutPaymentsResponseHandler $checkoutPaymentsDetailsHandler; private Payment|MockObject $paymentMock; private Order|MockObject $orderMock; private array $handlingSubject; @@ -21,7 +22,7 @@ class CheckoutPaymentsDetailsHandlerTest extends AbstractAdyenTestCase protected function setUp(): void { $this->adyenHelperMock = $this->createMock(Data::class); - $this->checkoutPaymentsDetailsHandler = new CheckoutPaymentsDetailsHandler($this->adyenHelperMock); + $this->checkoutPaymentsDetailsHandler = new CheckoutPaymentsResponseHandler($this->adyenHelperMock); $orderAdapterMock = $this->createMock(OrderAdapterInterface::class); $this->orderMock = $this->createMock(Order::class); @@ -80,7 +81,7 @@ public function testIfBoletoSendsAnEmail() $this->paymentMock ->expects($this->once()) ->method('getMethod') - ->willReturn(CheckoutPaymentsDetailsHandler::ADYEN_BOLETO); + ->willReturn(PaymentMethods::ADYEN_BOLETO); // for boleto it should not call this function. $this->orderMock diff --git a/etc/di.xml b/etc/di.xml index f0b364c07..03e44aaaa 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1358,7 +1358,7 @@ - Adyen\Payment\Gateway\Response\CheckoutPaymentsDetailsHandler + Adyen\Payment\Gateway\Response\CheckoutPaymentsResponseHandler Adyen\Payment\Gateway\Response\VaultDetailsHandler AdyenPaymentAuthorisationOrderStatusHistoryHandler Adyen\Payment\Gateway\Response\StateDataHandler @@ -1388,7 +1388,7 @@ - Adyen\Payment\Gateway\Response\CheckoutPaymentsDetailsHandler + Adyen\Payment\Gateway\Response\CheckoutPaymentsResponseHandler Adyen\Payment\Gateway\Response\VaultDetailsHandler From e76ea2618520cf434e6078ac56d0301a5d645b9a Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:08:37 +0100 Subject: [PATCH 18/42] [ECP-9639] Refactor PaymentLinks handlers and validators --- ...er.php => PaymentLinksResponseHandler.php} | 34 ++++++++-------- .../PaymentLinksResponseValidator.php | 39 ++----------------- etc/di.xml | 10 +---- 3 files changed, 22 insertions(+), 61 deletions(-) rename Gateway/Response/{PayByLinkResponseHandler.php => PaymentLinksResponseHandler.php} (69%) diff --git a/Gateway/Response/PayByLinkResponseHandler.php b/Gateway/Response/PaymentLinksResponseHandler.php similarity index 69% rename from Gateway/Response/PayByLinkResponseHandler.php rename to Gateway/Response/PaymentLinksResponseHandler.php index 2d8915451..68c0558e4 100644 --- a/Gateway/Response/PayByLinkResponseHandler.php +++ b/Gateway/Response/PaymentLinksResponseHandler.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2021 Adyen NV (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -13,36 +13,38 @@ use Adyen\Payment\Helper\Data; use Adyen\Payment\Model\Ui\AdyenPayByLinkConfigProvider; +use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; -class PayByLinkResponseHandler implements HandlerInterface +class PaymentLinksResponseHandler implements HandlerInterface { /** - * @var Data + * @param Data $adyenHelper */ - protected $adyenHelper; - public function __construct( - Data $adyenHelper - ) { - $this->adyenHelper = $adyenHelper; - } + protected readonly Data $adyenHelper + ) { } /** * @param array $handlingSubject * @param array $response + * @return void + * @throws LocalizedException */ - public function handle(array $handlingSubject, array $response) + public function handle(array $handlingSubject, array $response): void { $paymentDataObject = SubjectReader::readPayment($handlingSubject); - + /** @var Payment $payment */ $payment = $paymentDataObject->getPayment(); - // Set transaction to pending by default, wait for notification. - // This must match the status checked for expired orders/payments in - // Adyen\Payment\Cron\Providers\PayByLinkExpiredPaymentOrdersProvider::provide + // Set the transaction not to processing by default wait for notification $payment->setIsTransactionPending(true); + // Do not close the transaction, so you can do a cancel() and void + $payment->setIsTransactionClosed(false); + // Do not close the parent transaction + $payment->setShouldCloseParentTransaction(false); if (!empty($response['url'])) { $payment->setAdditionalInformation(AdyenPayByLinkConfigProvider::URL_KEY, $response['url']); @@ -55,9 +57,5 @@ public function handle(array $handlingSubject, array $response) if (!empty($response['id'])) { $payment->setAdditionalInformation(AdyenPayByLinkConfigProvider::ID_KEY, $response['id']); } - - // do not close transaction so you can do a cancel() and void - $payment->setIsTransactionClosed(false); - $payment->setShouldCloseParentTransaction(false); } } diff --git a/Gateway/Validator/PaymentLinksResponseValidator.php b/Gateway/Validator/PaymentLinksResponseValidator.php index 889e4cddf..293de2a24 100644 --- a/Gateway/Validator/PaymentLinksResponseValidator.php +++ b/Gateway/Validator/PaymentLinksResponseValidator.php @@ -1,21 +1,9 @@ @@ -23,9 +11,7 @@ namespace Adyen\Payment\Gateway\Validator; -use Adyen\Payment\Helper\Data; use Adyen\Payment\Logger\AdyenLogger; -use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Validator\AbstractValidator; use Magento\Payment\Gateway\Validator\ResultInterface; @@ -34,29 +20,13 @@ class PaymentLinksResponseValidator extends AbstractValidator { /** - * @var AdyenLogger - */ - private $adyenLogger; - - /** - * @var Data - */ - private $adyenHelper; - - /** - * CheckoutResponseValidator constructor. - * * @param ResultInterfaceFactory $resultFactory * @param AdyenLogger $adyenLogger - * @param Data $adyenHelper */ public function __construct( ResultInterfaceFactory $resultFactory, - AdyenLogger $adyenLogger, - Data $adyenHelper + private readonly AdyenLogger $adyenLogger ) { - $this->adyenLogger = $adyenLogger; - $this->adyenHelper = $adyenHelper; parent::__construct($resultFactory); } @@ -64,7 +34,7 @@ public function __construct( * @param array $validationSubject * @return ResultInterface */ - public function validate(array $validationSubject) + public function validate(array $validationSubject): ResultInterface { $response = SubjectReader::readResponse($validationSubject); @@ -78,8 +48,7 @@ public function validate(array $validationSubject) $this->adyenLogger->error($response['error']); } - $errorMsg = __('Error with payment method, please select a different payment method.'); - throw new LocalizedException($errorMsg); + $errorMessages[] = __('Error with payment method, please select a different payment method.'); } return $this->createResult($isValid, $errorMessages); diff --git a/etc/di.xml b/etc/di.xml index e93986ed2..696dce6eb 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -917,8 +917,8 @@ AdyenPaymentPayByLinkInitializeRequest Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPaymentLinks - PaymentLinksResponseValidator - Adyen\Payment\Gateway\Response\PayByLinkResponseHandler + Adyen\Payment\Gateway\Validator\PaymentLinksResponseValidator + Adyen\Payment\Gateway\Response\PaymentLinksResponseHandler @@ -1440,12 +1440,6 @@ - - - - Adyen\Payment\Logger\AdyenLogger - - Adyen\Payment\Logger\AdyenLogger From ab390a292baceef5b4e5d091146315560aec4639 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:14:40 +0100 Subject: [PATCH 19/42] [ECP-9639] Refactor DonateResponseValidator --- Gateway/Validator/DonateResponseValidator.php | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/Gateway/Validator/DonateResponseValidator.php b/Gateway/Validator/DonateResponseValidator.php index be613bb20..5abd3657e 100644 --- a/Gateway/Validator/DonateResponseValidator.php +++ b/Gateway/Validator/DonateResponseValidator.php @@ -3,7 +3,7 @@ * * Adyen Payment Module * - * Copyright (c) 2021 Adyen N.V. + * Copyright (c) 2025 Adyen N.V. * This file is open source and available under the MIT license. * See the LICENSE file for more info. * @@ -13,7 +13,6 @@ namespace Adyen\Payment\Gateway\Validator; use Adyen\Payment\Logger\AdyenLogger; -use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Validator\AbstractValidator; use Magento\Payment\Gateway\Validator\ResultInterface; @@ -21,33 +20,42 @@ class DonateResponseValidator extends AbstractValidator { + const VALID_RESULT_CODES = ['Authorised', 'Received']; + /** - * @var AdyenLogger + * @param ResultInterfaceFactory $resultInterfaceFactory + * @param AdyenLogger $adyenLogger */ - private $adyenLogger; - - public function __construct(ResultInterfaceFactory $resultInterfaceFactory, AdyenLogger $adyenLogger) - { + public function __construct( + ResultInterfaceFactory $resultInterfaceFactory, + private readonly AdyenLogger $adyenLogger + ) { parent::__construct($resultInterfaceFactory); - $this->adyenLogger = $adyenLogger; } /** - * @inheritDoc - * @throws LocalizedException + * @param array $validationSubject + * @return ResultInterface */ public function validate(array $validationSubject): ResultInterface { $response = SubjectReader::readResponse($validationSubject); - if (empty($response['payment']['resultCode'])) { + $isValid = true; + $errorMessages = []; + + if (empty($response['payment']['resultCode']) || + !in_array($response['payment']['resultCode'], self::VALID_RESULT_CODES)) { if (!empty($response['error'])) { - $this->adyenLogger->error($response['error']); + $this->adyenLogger->error( + sprintf("An error occurred with the donation: %s", $response['error']) + ); } - throw new LocalizedException(__('An error occurred with the donation.')); + $isValid = false; + $errorMessages[] = __('An error occurred with the donation.'); } - return $this->createResult(in_array($response['payment']['resultCode'], ['Authorised', 'Received'])); + return $this->createResult($isValid, $errorMessages); } } From 93ffab836a14312318e5defb24e10b620c6d6fb6 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:31:41 +0100 Subject: [PATCH 20/42] [ECP-9639] Update payment method vaults response handler and remove redundant response handler --- .../PaymentAuthorisationDetailsHandler.php | 53 ------------------- etc/di.xml | 10 +--- 2 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 Gateway/Response/PaymentAuthorisationDetailsHandler.php diff --git a/Gateway/Response/PaymentAuthorisationDetailsHandler.php b/Gateway/Response/PaymentAuthorisationDetailsHandler.php deleted file mode 100644 index 13e73e894..000000000 --- a/Gateway/Response/PaymentAuthorisationDetailsHandler.php +++ /dev/null @@ -1,53 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Magento\Payment\Gateway\Helper\SubjectReader; -use Magento\Payment\Gateway\Response\HandlerInterface; -use Magento\Sales\Api\Data\OrderPaymentInterface; - -class PaymentAuthorisationDetailsHandler implements HandlerInterface -{ - /** - * @param array $handlingSubject - * @param array $responseCollection - */ - public function handle(array $handlingSubject, array $responseCollection): void - { - $payment = SubjectReader::readPayment($handlingSubject); - - /** @var OrderPaymentInterface $payment */ - $payment = $payment->getPayment(); - - // set transaction not to processing by default wait for notification - $payment->setIsTransactionPending(true); - - // no not send order confirmation mail - $payment->getOrder()->setCanSendNewEmailFlag(false); - - // for partial payments, non-giftcard payments will always be the last element in the collection - // for non-partial, there is only one response in the collection - $response = end($responseCollection); - if (!empty($response['pspReference'])) { - // set pspReference as transactionId - $payment->setCcTransId($response['pspReference']); - $payment->setLastTransId($response['pspReference']); - - // set transaction - $payment->setTransactionId($response['pspReference']); - } - - // do not close transaction so you can do a cancel() and void - $payment->setIsTransactionClosed(false); - $payment->setShouldCloseParentTransaction(false); - } -} diff --git a/etc/di.xml b/etc/di.xml index 696dce6eb..669bcabf5 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -887,7 +887,7 @@ Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPayment Adyen\Payment\Gateway\Validator\CheckoutResponseValidator - AdyenPaymentVaultResponseHandlerComposite + AdyenPaymentResponseHandlerComposite @@ -1171,14 +1171,6 @@ - - - - Adyen\Payment\Gateway\Response\PaymentAuthorisationDetailsHandler - Adyen\Payment\Gateway\Response\PaymentCommentHistoryHandler - - - From 987c4f53e1dc95df83a4ce86302b796df096d0d8 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:32:24 +0100 Subject: [PATCH 21/42] [ECP-9639] Remove redundant response handler --- .../PaymentCommentHistoryRefundHandler.php | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 Gateway/Response/PaymentCommentHistoryRefundHandler.php diff --git a/Gateway/Response/PaymentCommentHistoryRefundHandler.php b/Gateway/Response/PaymentCommentHistoryRefundHandler.php deleted file mode 100644 index f388872f0..000000000 --- a/Gateway/Response/PaymentCommentHistoryRefundHandler.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Magento\Payment\Gateway\Response\HandlerInterface; - -class PaymentCommentHistoryRefundHandler implements HandlerInterface -{ - /** - * @param array $handlingSubject - * @param array $response - * @return $this - */ - public function handle(array $handlingSubject, array $response) - { - $readPayment = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($handlingSubject); - - $payment = $readPayment->getPayment(); - - foreach ($response as $singleResponse) { - if (isset($singleResponse['resultCode'])) { - $responseCode = $singleResponse['resultCode']; - } else { - // try to get response from response key (used for modifications - if (isset($singleResponse['response'])) { - $responseCode = $singleResponse['response']; - } else { - $responseCode = ""; - } - } - - if (isset($singleResponse['pspReference'])) { - $pspReference = $singleResponse['pspReference']; - } else { - $pspReference = ""; - } - - $type = 'Adyen Result response:'; - $comment = __( - '%1
authResult: %2
pspReference: %3 ', - $type, - $responseCode, - $pspReference - ); - - if ($responseCode) { - $payment->getOrder()->setAdyenResulturlEventCode($responseCode); - } - - $payment->getOrder()->addStatusHistoryComment($comment, $payment->getOrder()->getStatus()); - } - - return $this; - } -} From 3c216a2381879e8a5ac80c8cefab667be4beae82 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:33:35 +0100 Subject: [PATCH 22/42] [ECP-9639] Rename the class only used by capture command pool --- ...mmentHistoryHandler.php => CaptureCommentHistoryHandler.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Gateway/Response/{PaymentCommentHistoryHandler.php => CaptureCommentHistoryHandler.php} (98%) diff --git a/Gateway/Response/PaymentCommentHistoryHandler.php b/Gateway/Response/CaptureCommentHistoryHandler.php similarity index 98% rename from Gateway/Response/PaymentCommentHistoryHandler.php rename to Gateway/Response/CaptureCommentHistoryHandler.php index 1bce11c9f..8883c425e 100644 --- a/Gateway/Response/PaymentCommentHistoryHandler.php +++ b/Gateway/Response/CaptureCommentHistoryHandler.php @@ -15,7 +15,7 @@ use Adyen\Payment\Logger\AdyenLogger; use Magento\Payment\Gateway\Response\HandlerInterface; -class PaymentCommentHistoryHandler implements HandlerInterface +class CaptureCommentHistoryHandler implements HandlerInterface { /** @var AdyenLogger $adyenLogger */ private $adyenLogger; From bd4f6a3129cf29517374dde14f999608becc3ea1 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:38:23 +0100 Subject: [PATCH 23/42] [ECP-9639] Move argument validator to the constructor --- .../AbstractOrderStatusHistoryHandler.php | 25 +++++++++++++------ .../AbstractModificationResponseValidator.php | 8 ++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Gateway/Response/AbstractOrderStatusHistoryHandler.php b/Gateway/Response/AbstractOrderStatusHistoryHandler.php index f0c94c543..a3b8083cc 100644 --- a/Gateway/Response/AbstractOrderStatusHistoryHandler.php +++ b/Gateway/Response/AbstractOrderStatusHistoryHandler.php @@ -21,30 +21,39 @@ class AbstractOrderStatusHistoryHandler implements HandlerInterface /** * @param string $eventType Indicates the API call event type (authorization, capture etc.) * @param OrderStatusHistory $orderStatusHistoryHelper + * @throws AdyenException */ public function __construct( protected readonly string $eventType, protected readonly OrderStatusHistory $orderStatusHistoryHelper - ) { } + ) { + if (empty($eventType)) { + throw new AdyenException( + __('Order status history can not be handled due to missing constructor argument!') + ); + } + } /** * @throws AdyenException */ public function handle(array $handlingSubject, array $responseCollection): void { - if (empty($this->eventType)) { - throw new AdyenException( - __('Order status history can not be handled due to missing event type!') - ); - } - $readPayment = SubjectReader::readPayment($handlingSubject); $payment = $readPayment->getPayment(); $order = $payment->getOrder(); foreach ($responseCollection as $response) { - $comment = $this->orderStatusHistoryHelper->buildApiResponseComment($response, $this->eventType); + $comment = $this->orderStatusHistoryHelper->buildApiResponseComment($response, $this->getEventType()); $order->addCommentToStatusHistory($comment, $order->getStatus()); } } + + /** + * @return string + */ + private function getEventType(): string + { + return $this->eventType; + } } diff --git a/Gateway/Validator/AbstractModificationResponseValidator.php b/Gateway/Validator/AbstractModificationResponseValidator.php index db887fd3e..0bcdf7eba 100644 --- a/Gateway/Validator/AbstractModificationResponseValidator.php +++ b/Gateway/Validator/AbstractModificationResponseValidator.php @@ -11,6 +11,7 @@ namespace Adyen\Payment\Gateway\Validator; +use Adyen\AdyenException; use Adyen\Payment\Logger\AdyenLogger; use Magento\Framework\Exception\ValidatorException; use Magento\Payment\Gateway\Helper\SubjectReader; @@ -25,6 +26,7 @@ abstract class AbstractModificationResponseValidator extends AbstractValidator * @param AdyenLogger $adyenLogger * @param string $modificationType * @param array $validStatuses + * @throws AdyenException */ public function __construct( ResultInterfaceFactory $resultFactory, @@ -32,6 +34,12 @@ public function __construct( private readonly string $modificationType, private readonly array $validStatuses ) { + if (empty($modificationType) || empty($validStatuses)) { + throw new AdyenException( + __('Modification response can not be handled due to missing constructor arguments!') + ); + } + parent::__construct($resultFactory); } From 801a9cb5f2f626c792fe0f85576646803ad04868 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:55:53 +0100 Subject: [PATCH 24/42] [ECP-9639] Refactor refunds response handler --- .../Client/TransactionRefundInterface.php | 1 + .../ModificationsRefundsResponseHandler.php | 66 +++++++++++++++ .../Response/PaymentRefundDetailsHandler.php | 81 ------------------- etc/di.xml | 2 +- 4 files changed, 68 insertions(+), 82 deletions(-) create mode 100644 Gateway/Response/ModificationsRefundsResponseHandler.php delete mode 100644 Gateway/Response/PaymentRefundDetailsHandler.php diff --git a/Gateway/Http/Client/TransactionRefundInterface.php b/Gateway/Http/Client/TransactionRefundInterface.php index 2a77478cc..9bc473252 100644 --- a/Gateway/Http/Client/TransactionRefundInterface.php +++ b/Gateway/Http/Client/TransactionRefundInterface.php @@ -9,4 +9,5 @@ interface TransactionRefundInterface extends ClientInterface const REFUND_AMOUNT = 'refund_amount'; const REFUND_CURRENCY = 'refund_currency'; const ORIGINAL_REFERENCE = 'original_reference'; + const PSPREFERENCE = 'pspReference'; } diff --git a/Gateway/Response/ModificationsRefundsResponseHandler.php b/Gateway/Response/ModificationsRefundsResponseHandler.php new file mode 100644 index 000000000..710047d33 --- /dev/null +++ b/Gateway/Response/ModificationsRefundsResponseHandler.php @@ -0,0 +1,66 @@ + + */ + +namespace Adyen\Payment\Gateway\Response; + +use Adyen\Payment\Gateway\Http\Client\TransactionRefundInterface as TransactionRefund; +use Adyen\Payment\Helper\Creditmemo; +use Adyen\Payment\Helper\Data; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +class ModificationsRefundsResponseHandler implements HandlerInterface +{ + /** + * @param Creditmemo $creditmemoHelper + * @param Data $adyenDataHelper + */ + public function __construct( + private readonly Creditmemo $creditmemoHelper, + private readonly Data $adyenDataHelper + ) { } + + /** + * @param array $handlingSubject + * @param array $responseCollection + * @throws AlreadyExistsException + */ + public function handle(array $handlingSubject, array $responseCollection): void + { + $payment = SubjectReader::readPayment($handlingSubject); + /** @var Payment $payment */ + $payment = $payment->getPayment(); + + foreach ($responseCollection as $response) { + $payment->setLastTransId($response['pspReference']); + + $this->creditmemoHelper->createAdyenCreditMemo( + $payment, + $response[TransactionRefund::PSPREFERENCE], + $response[TransactionRefund::ORIGINAL_REFERENCE], + $this->adyenDataHelper->originalAmount( + $response[TransactionRefund::REFUND_AMOUNT], + $response[TransactionRefund::REFUND_CURRENCY] + ) + ); + } + + /** + * close current transaction because you have refunded the goods + * but only on full refund close the authorisation + */ + $payment->setIsTransactionClosed(true); + $closeParent = !$payment->getCreditmemo()->getInvoice()->canRefund(); + $payment->setShouldCloseParentTransaction($closeParent); + } +} diff --git a/Gateway/Response/PaymentRefundDetailsHandler.php b/Gateway/Response/PaymentRefundDetailsHandler.php deleted file mode 100644 index 4575ac218..000000000 --- a/Gateway/Response/PaymentRefundDetailsHandler.php +++ /dev/null @@ -1,81 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Adyen\Payment\Gateway\Http\Client\TransactionRefundInterface as TransactionRefund; -use Adyen\Payment\Helper\Creditmemo; -use Adyen\Payment\Helper\Data; -use Magento\Framework\Exception\AlreadyExistsException; -use Magento\Framework\Exception\LocalizedException; -use Magento\Payment\Gateway\Response\HandlerInterface; - -class PaymentRefundDetailsHandler implements HandlerInterface -{ - /** - * @var Creditmemo - */ - private $creditmemoHelper; - - /** @var Data */ - private $adyenDataHelper; - - public function __construct( - Creditmemo $creditmemoHelper, - Data $adyenDataHelper - ) { - $this->creditmemoHelper = $creditmemoHelper; - $this->adyenDataHelper = $adyenDataHelper; - } - - /** - * @param array $handlingSubject - * @param array $response - * @throws AlreadyExistsException|LocalizedException - */ - public function handle(array $handlingSubject, array $response) - { - $payment = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($handlingSubject); - - /** @var Order\Payment $payment */ - $payment = $payment->getPayment(); - - foreach ($response as $singleResponse) { - if (isset($singleResponse['error'])) { - throw new LocalizedException( - "The refund failed. Please make sure the amount is not greater than the limit or negative. - Otherwise, refer to the logs for details." - ); - } - - // set pspReference as lastTransId only! - $payment->setLastTransId($singleResponse['pspReference']); - - $this->creditmemoHelper->createAdyenCreditMemo( - $payment, - $singleResponse['pspReference'], - $singleResponse[TransactionRefund::ORIGINAL_REFERENCE], - $this->adyenDataHelper->originalAmount( - $singleResponse[TransactionRefund::REFUND_AMOUNT], - $singleResponse[TransactionRefund::REFUND_CURRENCY] - ) - ); - } - - /** - * close current transaction because you have refunded the goods - * but only on full refund close the authorisation - */ - $payment->setIsTransactionClosed(true); - $closeParent = !(bool)$payment->getCreditmemo()->getInvoice()->canRefund(); - $payment->setShouldCloseParentTransaction($closeParent); - } -} diff --git a/etc/di.xml b/etc/di.xml index 669bcabf5..793a6af47 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1353,7 +1353,7 @@ - Adyen\Payment\Gateway\Response\PaymentRefundDetailsHandler + Adyen\Payment\Gateway\Response\ModificationsRefundsResponseHandler AdyenPaymentRefundOrderStatusHistoryHandler From e4e0f2f626c7f66a479addcb37896b3429f2cb24 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:57:48 +0100 Subject: [PATCH 25/42] [ECP-9639] Refactor cancels response handler --- ...php => ModificationsCancelsResponseHandler.php} | 14 ++++++++------ etc/di.xml | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) rename Gateway/Response/{PaymentCancelDetailsHandler.php => ModificationsCancelsResponseHandler.php} (68%) diff --git a/Gateway/Response/PaymentCancelDetailsHandler.php b/Gateway/Response/ModificationsCancelsResponseHandler.php similarity index 68% rename from Gateway/Response/PaymentCancelDetailsHandler.php rename to Gateway/Response/ModificationsCancelsResponseHandler.php index 33d413280..b7317a773 100644 --- a/Gateway/Response/PaymentCancelDetailsHandler.php +++ b/Gateway/Response/ModificationsCancelsResponseHandler.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2015 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -11,19 +11,21 @@ namespace Adyen\Payment\Gateway\Response; +use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; -class PaymentCancelDetailsHandler implements HandlerInterface +class ModificationsCancelsResponseHandler implements HandlerInterface { /** * @param array $handlingSubject * @param array $response + * @return void */ - public function handle(array $handlingSubject, array $response) + public function handle(array $handlingSubject, array $response): void { - $payment = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($handlingSubject); - - /** @var OrderPaymentInterface $payment */ + $payment = SubjectReader::readPayment($handlingSubject); + /** @var Payment $payment */ $payment = $payment->getPayment(); // set pspReference as lastTransId only! diff --git a/etc/di.xml b/etc/di.xml index 793a6af47..5c112a743 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1361,7 +1361,7 @@ - Adyen\Payment\Gateway\Response\PaymentCancelDetailsHandler + Adyen\Payment\Gateway\Response\ModificationsCancelsResponseHandler AdyenPaymentCancellationOrderStatusHistoryHandler From ae6762b218cf283a91ba4032e3c98994880325f1 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 13:59:15 +0100 Subject: [PATCH 26/42] [ECP-9639] Rename the class --- ...ponseHandler.php => CheckoutPaymentLinksResponseHandler.php} | 2 +- etc/di.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Gateway/Response/{PaymentLinksResponseHandler.php => CheckoutPaymentLinksResponseHandler.php} (96%) diff --git a/Gateway/Response/PaymentLinksResponseHandler.php b/Gateway/Response/CheckoutPaymentLinksResponseHandler.php similarity index 96% rename from Gateway/Response/PaymentLinksResponseHandler.php rename to Gateway/Response/CheckoutPaymentLinksResponseHandler.php index 68c0558e4..a0156153f 100644 --- a/Gateway/Response/PaymentLinksResponseHandler.php +++ b/Gateway/Response/CheckoutPaymentLinksResponseHandler.php @@ -18,7 +18,7 @@ use Magento\Payment\Gateway\Response\HandlerInterface; use Magento\Sales\Model\Order\Payment; -class PaymentLinksResponseHandler implements HandlerInterface +class CheckoutPaymentLinksResponseHandler implements HandlerInterface { /** * @param Data $adyenHelper diff --git a/etc/di.xml b/etc/di.xml index 5c112a743..e0e6d9fde 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -918,7 +918,7 @@ Adyen\Payment\Gateway\Http\TransferFactory Adyen\Payment\Gateway\Http\Client\TransactionPaymentLinks Adyen\Payment\Gateway\Validator\PaymentLinksResponseValidator - Adyen\Payment\Gateway\Response\PaymentLinksResponseHandler + Adyen\Payment\Gateway\Response\CheckoutPaymentLinksResponseHandler
From d48ab2923b481d238881b501b55f8efa4ebb99f4 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 14:23:18 +0100 Subject: [PATCH 27/42] [ECP-9639] Refactor captures response handler --- .../ModificationsCapturesResponseHandler.php | 70 ++++++++++ .../Response/PaymentCaptureDetailsHandler.php | 130 ------------------ etc/di.xml | 2 +- 3 files changed, 71 insertions(+), 131 deletions(-) create mode 100644 Gateway/Response/ModificationsCapturesResponseHandler.php delete mode 100644 Gateway/Response/PaymentCaptureDetailsHandler.php diff --git a/Gateway/Response/ModificationsCapturesResponseHandler.php b/Gateway/Response/ModificationsCapturesResponseHandler.php new file mode 100644 index 000000000..fc7bc6c0e --- /dev/null +++ b/Gateway/Response/ModificationsCapturesResponseHandler.php @@ -0,0 +1,70 @@ + + */ + +namespace Adyen\Payment\Gateway\Response; + +use Adyen\Payment\Gateway\Http\Client\TransactionCapture; +use Adyen\Payment\Helper\Invoice; +use Adyen\Payment\Logger\AdyenLogger; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; + +class ModificationsCapturesResponseHandler implements HandlerInterface +{ + /** + * @param AdyenLogger $adyenLogger + * @param Invoice $invoiceHelper + */ + public function __construct( + private readonly AdyenLogger $adyenLogger, + private readonly Invoice $invoiceHelper + ) { } + + /** + * @param array $handlingSubject + * @param array $responseCollection + * @throws AlreadyExistsException + */ + public function handle(array $handlingSubject, array $responseCollection): void + { + $payment = SubjectReader::readPayment($handlingSubject); + /** @var Payment $payment */ + $payment = $payment->getPayment(); + + foreach ($responseCollection as $response) { + if (count($responseCollection) > 1) { + $this->adyenLogger->info(sprintf( + 'Handling partial OR multiple capture response in details handler for order %s', + $payment->getOrder()->getIncrementId() + )); + } + + if (!empty($response['pspReference'])) { + // Set transId to the last capture request + $payment->setLastTransId($response['pspReference']); + } + + $this->invoiceHelper->createAdyenInvoice( + $payment, + $response['pspReference'] ?? '', + $response[TransactionCapture::ORIGINAL_REFERENCE], + $response[TransactionCapture::CAPTURE_AMOUNT] + ); + } + + // Set the invoice status to pending and wait for CAPTURE webhook + $payment->setIsTransactionPending(true); + // Do not close parent authorisation since order can still be cancelled/refunded + $payment->setShouldCloseParentTransaction(false); + } +} diff --git a/Gateway/Response/PaymentCaptureDetailsHandler.php b/Gateway/Response/PaymentCaptureDetailsHandler.php deleted file mode 100644 index 8a01992eb..000000000 --- a/Gateway/Response/PaymentCaptureDetailsHandler.php +++ /dev/null @@ -1,130 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Adyen\Payment\Gateway\Http\Client\TransactionCapture; -use Adyen\Payment\Helper\Invoice; -use Adyen\Payment\Logger\AdyenLogger; -use Magento\Framework\Exception\AlreadyExistsException; -use Magento\Payment\Gateway\Response\HandlerInterface; -use Magento\Sales\Model\Order; - -class PaymentCaptureDetailsHandler implements HandlerInterface -{ - /** @var AdyenLogger $adyenLogger */ - private $adyenLogger; - - /** @var Invoice $invoiceHelper */ - private $invoiceHelper; - - /** - * PaymentCaptureDetailsHandler constructor. - * @param AdyenLogger $adyenLogger - * @param Invoice $invoiceHelper - */ - public function __construct(AdyenLogger $adyenLogger, Invoice $invoiceHelper) - { - $this->adyenLogger = $adyenLogger; - $this->invoiceHelper = $invoiceHelper; - } - - /** - * @param array $handlingSubject - * @param array $response - * @throws AlreadyExistsException - */ - public function handle(array $handlingSubject, array $response) - { - $payment = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($handlingSubject); - - /** @var Order\Payment $payment */ - $payment = $payment->getPayment(); - - if (array_key_exists(TransactionCapture::MULTIPLE_AUTHORIZATIONS, $response)) { - $this->handlePartialOrMultipleCaptureRequests($payment, $response); - } else { - // set pspReference as lastTransId only! - $payment->setLastTransId($response['pspReference']); - $this->invoiceHelper->createAdyenInvoice( - $payment, - $response['pspReference'], - $response[TransactionCapture::ORIGINAL_REFERENCE], - $response[TransactionCapture::CAPTURE_AMOUNT] - ); - - // The capture request will return a capture-received message, but it doesn't mean the capture has been final - // so the invoice is set to Pending - if ($response["status"] === TransactionCapture::CAPTURE_RECEIVED) { - $this->setInvoiceToPending($payment); - } - } - } - - /** - * @param Order\Payment $payment - * @param $responseContainer - * @throws AlreadyExistsException - */ - public function handlePartialOrMultipleCaptureRequests(Order\Payment $payment, $responseContainer) - { - $lastTransId = null; - $this->adyenLogger->info(sprintf( - 'Handling partial OR multiple capture response in details handler for order %s', - $payment->getOrder()->getIncrementId() - )); - - $captureNotReceived = []; - - foreach ($responseContainer[TransactionCapture::MULTIPLE_AUTHORIZATIONS] as $response) { - if ($response["status"] !== TransactionCapture::CAPTURE_RECEIVED) { - $captureNotReceived[] = $response['pspReference']; - } else { - $lastTransId = $response['pspReference']; - $this->invoiceHelper->createAdyenInvoice( - $payment, - $response['pspReference'], - $response[TransactionCapture::ORIGINAL_REFERENCE], - $response[TransactionCapture::CAPTURE_AMOUNT] - ); - } - } - - if (isset($lastTransId)) { - // Set transId to the last capture request - $payment->setLastTransId($lastTransId); - } - - if (empty($captureNotReceived)) { - $this->setInvoiceToPending($payment); - } else { - $this->adyenLogger->error(sprintf( - 'Response for transactions [%s] did not contain [capture-received]', - implode(', ', $captureNotReceived) - )); - } - } - - /** - * Set payment to pending to ensure that the invoice is created in an OPEN state - * - * @param $payment - * @return mixed - */ - private function setInvoiceToPending($payment) - { - $payment->setIsTransactionPending(true); - // Do not close parent authorisation since order can still be cancelled/refunded - $payment->setShouldCloseParentTransaction(false); - - return $payment; - } -} diff --git a/etc/di.xml b/etc/di.xml index e0e6d9fde..1e55a38e6 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1345,7 +1345,7 @@ - Adyen\Payment\Gateway\Response\PaymentCaptureDetailsHandler + Adyen\Payment\Gateway\Response\ModificationsCapturesResponseHandler Adyen\Payment\Gateway\Response\PaymentCommentHistoryHandler From cc1dd9a00a0c2c54305624d9ef15b9d432f3433c Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 14:28:25 +0100 Subject: [PATCH 28/42] [ECP-9639] Update lastTransId logic --- .../ModificationsCapturesResponseHandler.php | 7 ++----- .../ModificationsRefundsResponseHandler.php | 13 +++++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Gateway/Response/ModificationsCapturesResponseHandler.php b/Gateway/Response/ModificationsCapturesResponseHandler.php index fc7bc6c0e..df45aae16 100644 --- a/Gateway/Response/ModificationsCapturesResponseHandler.php +++ b/Gateway/Response/ModificationsCapturesResponseHandler.php @@ -44,15 +44,12 @@ public function handle(array $handlingSubject, array $responseCollection): void foreach ($responseCollection as $response) { if (count($responseCollection) > 1) { $this->adyenLogger->info(sprintf( - 'Handling partial OR multiple capture response in details handler for order %s', + 'Handling partial or multiple capture response for order %s', $payment->getOrder()->getIncrementId() )); } - if (!empty($response['pspReference'])) { - // Set transId to the last capture request - $payment->setLastTransId($response['pspReference']); - } + $payment->setLastTransId($response['pspReference']); $this->invoiceHelper->createAdyenInvoice( $payment, diff --git a/Gateway/Response/ModificationsRefundsResponseHandler.php b/Gateway/Response/ModificationsRefundsResponseHandler.php index 710047d33..6bb9653c2 100644 --- a/Gateway/Response/ModificationsRefundsResponseHandler.php +++ b/Gateway/Response/ModificationsRefundsResponseHandler.php @@ -14,6 +14,7 @@ use Adyen\Payment\Gateway\Http\Client\TransactionRefundInterface as TransactionRefund; use Adyen\Payment\Helper\Creditmemo; use Adyen\Payment\Helper\Data; +use Adyen\Payment\Logger\AdyenLogger; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; @@ -27,7 +28,8 @@ class ModificationsRefundsResponseHandler implements HandlerInterface */ public function __construct( private readonly Creditmemo $creditmemoHelper, - private readonly Data $adyenDataHelper + private readonly Data $adyenDataHelper, + private readonly AdyenLogger $adyenLogger ) { } /** @@ -42,7 +44,14 @@ public function handle(array $handlingSubject, array $responseCollection): void $payment = $payment->getPayment(); foreach ($responseCollection as $response) { - $payment->setLastTransId($response['pspReference']); + if (count($responseCollection) > 1) { + $this->adyenLogger->info(sprintf( + 'Handling partial or multiple refund response for order %s', + $payment->getOrder()->getIncrementId() + )); + } + + $payment->setLastTransId($response[TransactionRefund::PSPREFERENCE]); $this->creditmemoHelper->createAdyenCreditMemo( $payment, From d464908d31ede287213ceb266f97c70f59c2e746 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 14:37:05 +0100 Subject: [PATCH 29/42] [ECP-9639] Add required field validator to modifications response validators --- .../AbstractModificationResponseValidator.php | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Gateway/Validator/AbstractModificationResponseValidator.php b/Gateway/Validator/AbstractModificationResponseValidator.php index 0bcdf7eba..235e74ccb 100644 --- a/Gateway/Validator/AbstractModificationResponseValidator.php +++ b/Gateway/Validator/AbstractModificationResponseValidator.php @@ -21,6 +21,11 @@ abstract class AbstractModificationResponseValidator extends AbstractValidator { + const REQUIRED_RESPONSE_FIELDS = [ + 'status', + 'pspReference' + ]; + /** * @param ResultInterfaceFactory $resultFactory * @param AdyenLogger $adyenLogger @@ -60,7 +65,24 @@ public function validate(array $validationSubject): ResultInterface $errorMessages = []; foreach ($responseCollection as $response) { - if (empty($response['status']) || !in_array($response['status'], $this->getValidStatuses())) { + foreach (self::REQUIRED_RESPONSE_FIELDS as $requiredField) { + if (!array_key_exists($requiredField, $response)) { + $errorMessage = __( + '%1 field is missing in %2 response.', + $requiredField, + $this->getModificationType() + ); + + $this->adyenLogger->error($errorMessage); + + $isValid = false; + $errorMessages[] = $errorMessage; + + break; + } + } + + if (!in_array($response['status'], $this->getValidStatuses())) { $errorMessage = __( 'An error occurred while validating the %1 response', $this->getModificationType() From f797659ef9c7237a9aa5b7de755dc9edb8f2f0c1 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 14:38:59 +0100 Subject: [PATCH 30/42] [ECP-9639] Formatting --- Gateway/Response/ModificationsRefundsResponseHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gateway/Response/ModificationsRefundsResponseHandler.php b/Gateway/Response/ModificationsRefundsResponseHandler.php index 6bb9653c2..8886844ed 100644 --- a/Gateway/Response/ModificationsRefundsResponseHandler.php +++ b/Gateway/Response/ModificationsRefundsResponseHandler.php @@ -25,6 +25,7 @@ class ModificationsRefundsResponseHandler implements HandlerInterface /** * @param Creditmemo $creditmemoHelper * @param Data $adyenDataHelper + * @param AdyenLogger $adyenLogger */ public function __construct( private readonly Creditmemo $creditmemoHelper, @@ -69,7 +70,6 @@ public function handle(array $handlingSubject, array $responseCollection): void * but only on full refund close the authorisation */ $payment->setIsTransactionClosed(true); - $closeParent = !$payment->getCreditmemo()->getInvoice()->canRefund(); - $payment->setShouldCloseParentTransaction($closeParent); + $payment->setShouldCloseParentTransaction(!$payment->getCreditmemo()->getInvoice()->canRefund()); } } From ae344097cdef2849238ff0ea7003b823f1d07c14 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 14:56:07 +0100 Subject: [PATCH 31/42] [ECP-9639] Merge VaultDetailsHandler into CheckoutPaymentsResponseHandler --- .../CheckoutPaymentsResponseHandler.php | 7 +- Gateway/Response/VaultDetailsHandler.php | 59 ------------ .../Response/VaultDetailsHandlerTest.php | 96 ------------------- 3 files changed, 5 insertions(+), 157 deletions(-) delete mode 100644 Gateway/Response/VaultDetailsHandler.php delete mode 100644 Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php diff --git a/Gateway/Response/CheckoutPaymentsResponseHandler.php b/Gateway/Response/CheckoutPaymentsResponseHandler.php index 04786fc8a..dca1706c0 100644 --- a/Gateway/Response/CheckoutPaymentsResponseHandler.php +++ b/Gateway/Response/CheckoutPaymentsResponseHandler.php @@ -11,8 +11,8 @@ namespace Adyen\Payment\Gateway\Response; -use Adyen\Payment\Helper\Data; use Adyen\Payment\Helper\PaymentMethods; +use Adyen\Payment\Helper\Vault; use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; @@ -21,7 +21,7 @@ class CheckoutPaymentsResponseHandler implements HandlerInterface { public function __construct( - protected readonly Data $adyenHelper + private readonly Vault $vaultHelper ) { } /** @@ -79,6 +79,9 @@ public function handle(array $handlingSubject, array $responseCollection): void $payment->setAdditionalInformation('donationToken', $response['donationToken']); } + // Handle recurring payment details + $this->vaultHelper->handlePaymentResponseRecurringDetails($payment, $response); + // Do not send order confirmation email for Boleto payments if ($payment->getMethod() != PaymentMethods::ADYEN_BOLETO) { $payment->getOrder()->setCanSendNewEmailFlag(false); diff --git a/Gateway/Response/VaultDetailsHandler.php b/Gateway/Response/VaultDetailsHandler.php deleted file mode 100644 index 7b34b5ae5..000000000 --- a/Gateway/Response/VaultDetailsHandler.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Adyen\Payment\Helper\Vault; -use Magento\Framework\Exception\LocalizedException; -use Magento\Payment\Gateway\Data\PaymentDataObject; -use Magento\Payment\Gateway\Helper\SubjectReader; -use Magento\Payment\Gateway\Response\HandlerInterface; -use Magento\Sales\Model\Order\Payment; - -class VaultDetailsHandler implements HandlerInterface -{ - /** - * @var Vault - */ - private Vault $vaultHelper; - - /** - * @param Vault $vaultHelper - */ - public function __construct(Vault $vaultHelper) - { - $this->vaultHelper = $vaultHelper; - } - - /** - * @param array $handlingSubject - * @param array $responseCollection - * @return void - */ - public function handle(array $handlingSubject, array $responseCollection): void - { - // for (non-) partial payments, the non-giftcard payment is always last. - $response = end($responseCollection); - - // payments without additional data or only giftcards should be ignored. - if (empty($response['additionalData']) || $responseCollection['hasOnlyGiftCards']) { - return; - } - - /** @var PaymentDataObject $orderPayment */ - $orderPayment = SubjectReader::readPayment($handlingSubject); - /** @var Payment $payment */ - $payment = $orderPayment->getPayment(); - - // Handle recurring details - $this->vaultHelper->handlePaymentResponseRecurringDetails($payment, $response); - } -} diff --git a/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php b/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php deleted file mode 100644 index 206fdd275..000000000 --- a/Test/Unit/Gateway/Response/VaultDetailsHandlerTest.php +++ /dev/null @@ -1,96 +0,0 @@ -vaultHelperMock = $this->createMock(Vault::class); - $this->vaultDetailsHandler = new VaultDetailsHandler($this->vaultHelperMock); - - $orderAdapterMock = $this->createMock(OrderAdapterInterface::class); - $orderMock = $this->createMock(Order::class); - - $this->paymentMock = $this->createMock(Payment::class); - $this->paymentMock->method('getOrder')->willReturn($orderMock); - $this->paymentDataObject = new PaymentDataObject($orderAdapterMock, $this->paymentMock); - - $this->handlingSubject = [ - 'payment' => $this->paymentDataObject, - 'paymentAction' => "authorize", - 'stateObject' => null - ]; - } - - public function testIfGeneralFlowIsHandledCorrectly() - { - // prepare Handler input. - $responseCollection = [ - 'hasOnlyGiftCards' => false, - 0 => [ - 'additionalData' => ['someData' => 'value'], - 'amount' => [], - 'resultCode' => 'Authorised', - ] - ]; - - // Ensure the vaultHelper's method is called once with the correct arguments. - $this->vaultHelperMock - ->expects($this->once()) - ->method('handlePaymentResponseRecurringDetails') - ->with($this->paymentMock, $responseCollection[0]); - - $this->vaultDetailsHandler->handle($this->handlingSubject, $responseCollection); - } - - public function testIfPaymentsWithoutAdditionalDataAreIgnored() - { - // Prepare a responseCollection without additionalData - $responseCollection = [ - 'hasOnlyGiftCards' => false, - 0 => [ - 'additionalData' => [], - 'amount' => [], - 'resultCode' => 'Authorised', - ] - ]; - - // Ensure the vaultHelper's method is NOT called since additionalData is empty - $this->vaultHelperMock - ->expects($this->never()) - ->method('handlePaymentResponseRecurringDetails'); - - $this->vaultDetailsHandler->handle($this->handlingSubject, $responseCollection); - } - - public function testIfGiftCardOnlyPaymentsAreIgnored() - { - $responseCollection = [ - 'hasOnlyGiftCards' => true, - 'additionalData' => ['someData' => 'value'], - 'amount' => [], - 'resultCode' => 'Authorised', - ]; - - // Ensure the vaultHelper's method is NOT called since additionalData is empty - $this->vaultHelperMock - ->expects($this->never()) - ->method('handlePaymentResponseRecurringDetails'); - - $this->vaultDetailsHandler->handle($this->handlingSubject, $responseCollection); - } -} From 2112a3581df2c53a6c0a9d013b29eb56c0f08da2 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 14:56:24 +0100 Subject: [PATCH 32/42] [ECP-9639] Refactor state data handler --- ...hp => CheckoutStateDataCleanupHandler.php} | 42 +++++++++---------- etc/di.xml | 3 +- 2 files changed, 22 insertions(+), 23 deletions(-) rename Gateway/Response/{StateDataHandler.php => CheckoutStateDataCleanupHandler.php} (52%) diff --git a/Gateway/Response/StateDataHandler.php b/Gateway/Response/CheckoutStateDataCleanupHandler.php similarity index 52% rename from Gateway/Response/StateDataHandler.php rename to Gateway/Response/CheckoutStateDataCleanupHandler.php index 74c25d896..102f3e1f6 100644 --- a/Gateway/Response/StateDataHandler.php +++ b/Gateway/Response/CheckoutStateDataCleanupHandler.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2022 Adyen N.V. (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -13,42 +13,42 @@ use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\ResourceModel\StateData; -use Adyen\Payment\Model\ResourceModel\StateData\Collection; +use Adyen\Payment\Model\ResourceModel\StateData\Collection as StateDataCollection; use Exception; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; -class StateDataHandler implements HandlerInterface +class CheckoutStateDataCleanupHandler implements HandlerInterface { - private Collection $adyenStateData; - private StateData $stateDataResourceModel; - private AdyenLogger $adyenLogger; - + /** + * @param StateDataCollection $adyenStateDataCollection + * @param StateData $stateDataResourceModel + * @param AdyenLogger $adyenLogger + */ public function __construct( - Collection $adyenStateData, - StateData $stateDataResourceModel, - AdyenLogger $adyenLogger - ) { - $this->adyenStateData = $adyenStateData; - $this->stateDataResourceModel = $stateDataResourceModel; - $this->adyenLogger = $adyenLogger; - } - - public function handle(array $handlingSubject, array $response): self + private readonly StateDataCollection $adyenStateDataCollection, + private readonly StateData $stateDataResourceModel, + private readonly AdyenLogger $adyenLogger + ) { } + + /** + * @param array $handlingSubject + * @param array $response + * @return void + */ + public function handle(array $handlingSubject, array $response): void { $readPayment = SubjectReader::readPayment($handlingSubject); $quoteId = $readPayment->getPayment()->getOrder()->getQuoteId(); - $stateDataCollection = $this->adyenStateData->getStateDataRowsWithQuoteId($quoteId); + $stateDataCollection = $this->adyenStateDataCollection->getStateDataRowsWithQuoteId($quoteId); - foreach ($stateDataCollection->getIterator() as $stateDataItem) { + foreach ($stateDataCollection as $stateDataItem) { try { $this->stateDataResourceModel->delete($stateDataItem); } catch (Exception $exception) { $this->adyenLogger->error(__("State data was not cleaned-up: %s", $exception->getMessage())); } } - - return $this; } } diff --git a/etc/di.xml b/etc/di.xml index 1e55a38e6..42b7fefbb 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1309,9 +1309,8 @@ Adyen\Payment\Gateway\Response\CheckoutPaymentsResponseHandler - Adyen\Payment\Gateway\Response\VaultDetailsHandler AdyenPaymentAuthorisationOrderStatusHistoryHandler - Adyen\Payment\Gateway\Response\StateDataHandler + Adyen\Payment\Gateway\Response\CheckoutStateDataCleanupHandler From 08b61883d1b5070c2099fe87dda29096698a3f9f Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 15:00:08 +0100 Subject: [PATCH 33/42] [ECP-9639] Use abstract order status history handler for captures --- .../AbstractOrderStatusHistoryHandler.php | 2 +- .../Response/CaptureCommentHistoryHandler.php | 147 ------------------ etc/di.xml | 2 +- 3 files changed, 2 insertions(+), 149 deletions(-) delete mode 100644 Gateway/Response/CaptureCommentHistoryHandler.php diff --git a/Gateway/Response/AbstractOrderStatusHistoryHandler.php b/Gateway/Response/AbstractOrderStatusHistoryHandler.php index a3b8083cc..941b18db3 100644 --- a/Gateway/Response/AbstractOrderStatusHistoryHandler.php +++ b/Gateway/Response/AbstractOrderStatusHistoryHandler.php @@ -16,7 +16,7 @@ use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; -class AbstractOrderStatusHistoryHandler implements HandlerInterface +abstract class AbstractOrderStatusHistoryHandler implements HandlerInterface { /** * @param string $eventType Indicates the API call event type (authorization, capture etc.) diff --git a/Gateway/Response/CaptureCommentHistoryHandler.php b/Gateway/Response/CaptureCommentHistoryHandler.php deleted file mode 100644 index 8883c425e..000000000 --- a/Gateway/Response/CaptureCommentHistoryHandler.php +++ /dev/null @@ -1,147 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Response; - -use Adyen\Payment\Gateway\Http\Client\TransactionCapture; -use Adyen\Payment\Logger\AdyenLogger; -use Magento\Payment\Gateway\Response\HandlerInterface; - -class CaptureCommentHistoryHandler implements HandlerInterface -{ - /** @var AdyenLogger $adyenLogger */ - private $adyenLogger; - - /** - * PaymentCaptureDetailsHandler constructor. - * @param AdyenLogger $adyenLogger - */ - public function __construct(AdyenLogger $adyenLogger) - { - $this->adyenLogger = $adyenLogger; - } - - /** - * @param array $handlingSubject - * @param array $response - * @return $this - */ - public function handle(array $handlingSubject, array $response) - { - $readPayment = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($handlingSubject); - - $payment = $readPayment->getPayment(); - - if (array_key_exists(TransactionCapture::MULTIPLE_AUTHORIZATIONS, $response)) { - return $this->handlePartialOrMultipleCaptureRequests($payment, $response); - } - - $responseCode = $this->getResponseCode($response); - $pspReference = $this->getPspReference($response); - - $type = 'Adyen Result response:'; - $comment = __( - '%1
authResult: %2
pspReference: %3 ', - $type, - $responseCode, - $pspReference - ); - - if ($responseCode) { - $payment->getOrder()->setAdyenResulturlEventCode($responseCode); - } - - $payment->getOrder()->addStatusHistoryComment($comment, $payment->getOrder()->getStatus()); - - return $this; - } - - /** - * Handle multiple capture requests by creating a comment for each request, and adding all the event codes - * in the order model - * - * @param $payment - * @param array $responseContainer - * @return $this - */ - private function handlePartialOrMultipleCaptureRequests($payment, array $responseContainer) - { - $this->adyenLogger->info(sprintf( - 'Handling partial OR multiple capture response in comment history handler for order %s', - $payment->getOrder()->getIncrementId() - )); - - $resultEventCodes = []; - foreach ($responseContainer[TransactionCapture::MULTIPLE_AUTHORIZATIONS] as $response) { - $responseCode = $this->getResponseCode($response); - $pspReference = $this->getPspReference($response); - $amount = $response[TransactionCapture::FORMATTED_CAPTURE_AMOUNT]; - - $type = 'Adyen Result response:'; - $comment = __( - '%1
authResult: %2
pspReference: %3
amount: %4 ', - $type, - $responseCode, - $pspReference, - $amount - ); - - $resultEventCodes[] = $responseCode; - - $payment->getOrder()->addStatusHistoryComment($comment, $payment->getOrder()->getStatus()); - } - - if (!empty($resultEventCodes)) { - $payment->getOrder()->setAdyenResulturlEventCode(implode(', ', $resultEventCodes)); - } - - return $this; - } - - /** - * Search for the response code in the passed response array - * - * @param $response - * @return string - */ - private function getResponseCode($response) - { - if (isset($response['resultCode'])) { - $responseCode = $response['resultCode']; - } else { - // try to get response from response key (used for modifications - if (isset($response['response'])) { - $responseCode = $response['response']; - } else { - $responseCode = ''; - } - } - - return $responseCode; - } - - /** - * Get the pspReference or return empty string if not found - * - * @param $response - * @return string - */ - private function getPspReference($response) - { - if (isset($response['pspReference'])) { - $pspReference = $response['pspReference']; - } else { - $pspReference = ''; - } - - return $pspReference; - } -} diff --git a/etc/di.xml b/etc/di.xml index 42b7fefbb..a32676341 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1345,7 +1345,7 @@ Adyen\Payment\Gateway\Response\ModificationsCapturesResponseHandler - Adyen\Payment\Gateway\Response\PaymentCommentHistoryHandler + AdyenPaymentCaptureOrderStatusHistoryHandler From 2dfae660e01d9133e5959a6c97dc3aa95532596c Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 16:19:27 +0100 Subject: [PATCH 34/42] [ECP-9639] Update valid status constants --- etc/di.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/di.xml b/etc/di.xml index a32676341..3301f0646 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1037,7 +1037,7 @@ capture - Adyen\Model\Checkout\PaymentCaptureResponse\PaymentCaptureResponse::STATUS_RECEIVED + Adyen\Model\Checkout\PaymentCaptureResponse::STATUS_RECEIVED @@ -1045,7 +1045,7 @@ refund - Adyen\Model\Checkout\PaymentRefundResponse\PaymentRefundResponse::STATUS_RECEIVED + Adyen\Model\Checkout\PaymentRefundResponse::STATUS_RECEIVED @@ -1053,7 +1053,7 @@ cancel - Adyen\Model\Checkout\PaymentCancelResponse\PaymentCancelResponse::STATUS_RECEIVED + Adyen\Model\Checkout\PaymentCancelResponse::STATUS_RECEIVED From abe09cb2897a9b497f71b1a92642269d27cc5f4d Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 16:19:42 +0100 Subject: [PATCH 35/42] [ECP-9639] Remove hasOnlyGiftCards flag on payments chain --- Gateway/Http/Client/TransactionPayment.php | 2 -- .../Gateway/Response/CheckoutPaymentsResponseHandlerTest.php | 3 --- 2 files changed, 5 deletions(-) diff --git a/Gateway/Http/Client/TransactionPayment.php b/Gateway/Http/Client/TransactionPayment.php index 1561c31fb..841fcfebc 100644 --- a/Gateway/Http/Client/TransactionPayment.php +++ b/Gateway/Http/Client/TransactionPayment.php @@ -120,7 +120,6 @@ public function placeRequest(TransferInterface $transferObject): array $client = $this->adyenHelper->initializeAdyenClientWithClientConfig($clientConfig); $service = $this->adyenHelper->initializePaymentsApi($client); $responseCollection = []; - $responseCollection['hasOnlyGiftCards'] = false; try { list($requestData, $giftcardResponseCollection) = $this->processGiftcards($requestData, $service); @@ -130,7 +129,6 @@ public function placeRequest(TransferInterface $transferObject): array $responseCollection = array_merge($responseCollection, $giftcardResponseCollection); if ($this->remainingOrderAmount === 0) { - $responseCollection['hasOnlyGiftCards'] = true; return $responseCollection; } } diff --git a/Test/Unit/Gateway/Response/CheckoutPaymentsResponseHandlerTest.php b/Test/Unit/Gateway/Response/CheckoutPaymentsResponseHandlerTest.php index f90632c22..3b610cbae 100644 --- a/Test/Unit/Gateway/Response/CheckoutPaymentsResponseHandlerTest.php +++ b/Test/Unit/Gateway/Response/CheckoutPaymentsResponseHandlerTest.php @@ -42,7 +42,6 @@ public function testIfGeneralFlowIsHandledCorrectly() { // prepare Handler input. $responseCollection = [ - 'hasOnlyGiftCards' => false, 0 => [ 'additionalData' => [], 'amount' => [], @@ -69,7 +68,6 @@ public function testIfBoletoSendsAnEmail() { // prepare Handler input. $responseCollection = [ - 'hasOnlyGiftCards' => false, 0 => [ 'additionalData' => [], 'amount' => [], @@ -98,7 +96,6 @@ public function testIfPartialPaymentHandlesLastPaymentResponse() { // prepare Handler input. $responseCollection = [ - 'hasOnlyGiftCards' => false, 0 => [ 'additionalData' => [], 'amount' => [], From c4e1a555e8fe7857b844e3c3dc94222d495fcb30 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 16:23:06 +0100 Subject: [PATCH 36/42] [ECP-9639] Remove abstract keyword from the classes to use DI --- Gateway/Response/AbstractOrderStatusHistoryHandler.php | 2 +- Gateway/Validator/AbstractModificationResponseValidator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gateway/Response/AbstractOrderStatusHistoryHandler.php b/Gateway/Response/AbstractOrderStatusHistoryHandler.php index 941b18db3..a3b8083cc 100644 --- a/Gateway/Response/AbstractOrderStatusHistoryHandler.php +++ b/Gateway/Response/AbstractOrderStatusHistoryHandler.php @@ -16,7 +16,7 @@ use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; -abstract class AbstractOrderStatusHistoryHandler implements HandlerInterface +class AbstractOrderStatusHistoryHandler implements HandlerInterface { /** * @param string $eventType Indicates the API call event type (authorization, capture etc.) diff --git a/Gateway/Validator/AbstractModificationResponseValidator.php b/Gateway/Validator/AbstractModificationResponseValidator.php index 235e74ccb..f87b4f3d2 100644 --- a/Gateway/Validator/AbstractModificationResponseValidator.php +++ b/Gateway/Validator/AbstractModificationResponseValidator.php @@ -19,7 +19,7 @@ use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; -abstract class AbstractModificationResponseValidator extends AbstractValidator +class AbstractModificationResponseValidator extends AbstractValidator { const REQUIRED_RESPONSE_FIELDS = [ 'status', From 61cf61ac6cbfc93289e86a2e5d1ba99de5b54d59 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 16:33:03 +0100 Subject: [PATCH 37/42] [ECP-9639] Remove the comment --- Gateway/Response/CheckoutPaymentsResponseHandler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Gateway/Response/CheckoutPaymentsResponseHandler.php b/Gateway/Response/CheckoutPaymentsResponseHandler.php index dca1706c0..cfe836508 100644 --- a/Gateway/Response/CheckoutPaymentsResponseHandler.php +++ b/Gateway/Response/CheckoutPaymentsResponseHandler.php @@ -82,7 +82,6 @@ public function handle(array $handlingSubject, array $responseCollection): void // Handle recurring payment details $this->vaultHelper->handlePaymentResponseRecurringDetails($payment, $response); - // Do not send order confirmation email for Boleto payments if ($payment->getMethod() != PaymentMethods::ADYEN_BOLETO) { $payment->getOrder()->setCanSendNewEmailFlag(false); } From aa67447ac1d7490a3e4b4c07f3f4360cf5b3c25e Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 16:33:10 +0100 Subject: [PATCH 38/42] [ECP-9639] Fix the class name --- ...dator.php => AbstractModificationsResponseValidator.php} | 2 +- etc/di.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename Gateway/Validator/{AbstractModificationResponseValidator.php => AbstractModificationsResponseValidator.php} (98%) diff --git a/Gateway/Validator/AbstractModificationResponseValidator.php b/Gateway/Validator/AbstractModificationsResponseValidator.php similarity index 98% rename from Gateway/Validator/AbstractModificationResponseValidator.php rename to Gateway/Validator/AbstractModificationsResponseValidator.php index f87b4f3d2..4f33c86a6 100644 --- a/Gateway/Validator/AbstractModificationResponseValidator.php +++ b/Gateway/Validator/AbstractModificationsResponseValidator.php @@ -19,7 +19,7 @@ use Magento\Payment\Gateway\Validator\ResultInterface; use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; -class AbstractModificationResponseValidator extends AbstractValidator +class AbstractModificationsResponseValidator extends AbstractValidator { const REQUIRED_RESPONSE_FIELDS = [ 'status', diff --git a/etc/di.xml b/etc/di.xml index 3301f0646..b7ebb8764 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1033,7 +1033,7 @@ - + capture @@ -1041,7 +1041,7 @@ - + refund @@ -1049,7 +1049,7 @@ - + cancel From 9e03866f7bb395de38674dd653e699a07c6f4164 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 11 Mar 2025 16:43:15 +0100 Subject: [PATCH 39/42] [ECP-9639] Fix the response type --- Gateway/Http/Client/TransactionCancel.php | 6 ++++-- Gateway/Response/ModificationsCancelsResponseHandler.php | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Gateway/Http/Client/TransactionCancel.php b/Gateway/Http/Client/TransactionCancel.php index f7d140646..5da8fb689 100644 --- a/Gateway/Http/Client/TransactionCancel.php +++ b/Gateway/Http/Client/TransactionCancel.php @@ -58,7 +58,7 @@ public function placeRequest(TransferInterface $transferObject): array $client = $this->adyenHelper->initializeAdyenClientWithClientConfig($clientConfig); $service = $this->adyenHelper->initializeModificationsApi($client); - $responseData = []; + $responseCollection = []; foreach ($requests as $request) { $idempotencyKey = $this->idempotencyHelper->generateIdempotencyKey( @@ -83,8 +83,10 @@ public function placeRequest(TransferInterface $transferObject): array $responseData['error'] = $e->getMessage(); $this->adyenHelper->logAdyenException($e); } + + $responseCollection[] = $responseData; } - return $responseData; + return $responseCollection; } } diff --git a/Gateway/Response/ModificationsCancelsResponseHandler.php b/Gateway/Response/ModificationsCancelsResponseHandler.php index b7317a773..8a7277bfe 100644 --- a/Gateway/Response/ModificationsCancelsResponseHandler.php +++ b/Gateway/Response/ModificationsCancelsResponseHandler.php @@ -19,17 +19,18 @@ class ModificationsCancelsResponseHandler implements HandlerInterface { /** * @param array $handlingSubject - * @param array $response + * @param array $responseCollection * @return void */ - public function handle(array $handlingSubject, array $response): void + public function handle(array $handlingSubject, array $responseCollection): void { $payment = SubjectReader::readPayment($handlingSubject); /** @var Payment $payment */ $payment = $payment->getPayment(); - // set pspReference as lastTransId only! - $payment->setLastTransId($response['pspReference']); + foreach ($responseCollection as $response) { + $payment->setLastTransId($response['pspReference']); + } // close transaction because you have cancelled the transaction $payment->setIsTransactionClosed(true); From 7e720720dd5fb8b6211b415178b03fb425705211 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Wed, 12 Mar 2025 09:13:33 +0100 Subject: [PATCH 40/42] [ECP-9639] Refactor Cancel command chain --- Gateway/Http/Client/TransactionCancel.php | 21 +---- Gateway/Request/CancelDataBuilder.php | 27 ++---- ...AbstractModificationsResponseValidator.php | 86 ++++++++++++------- 3 files changed, 67 insertions(+), 67 deletions(-) diff --git a/Gateway/Http/Client/TransactionCancel.php b/Gateway/Http/Client/TransactionCancel.php index 5da8fb689..f4ab3e0e8 100644 --- a/Gateway/Http/Client/TransactionCancel.php +++ b/Gateway/Http/Client/TransactionCancel.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2023 Adyen N.V. (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -22,27 +22,14 @@ class TransactionCancel implements ClientInterface { - /** - * @var Data - */ - private Data $adyenHelper; - - /** - * @var Idempotency - */ - private Idempotency $idempotencyHelper; - /** * @param Data $adyenHelper * @param Idempotency $idempotencyHelper */ public function __construct( - Data $adyenHelper, - Idempotency $idempotencyHelper - ) { - $this->adyenHelper = $adyenHelper; - $this->idempotencyHelper = $idempotencyHelper; - } + private readonly Data $adyenHelper, + private readonly Idempotency $idempotencyHelper + ) { } /** * @param TransferInterface $transferObject diff --git a/Gateway/Request/CancelDataBuilder.php b/Gateway/Request/CancelDataBuilder.php index 9aca8481a..45f76fa45 100644 --- a/Gateway/Request/CancelDataBuilder.php +++ b/Gateway/Request/CancelDataBuilder.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2015 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -16,31 +16,20 @@ use Magento\Payment\Gateway\Request\BuilderInterface; use Adyen\Payment\Model\ResourceModel\Order\Payment; use Adyen\Payment\Helper\Data; -use Adyen\Payment\Helper\Config; /** * Class CustomerDataBuilder */ class CancelDataBuilder implements BuilderInterface { - /** @var Payment $adyenPaymentResourceModel */ - private $adyenPaymentResourceModel; - - /** @var Config $configHelper */ - private $configHelper; - - /** @var Data $adyenHelper */ - private $adyenHelper; - + /** + * @param Payment $adyenPaymentResourceModel + * @param Data $adyenHelper + */ public function __construct( - Payment $adyenPaymentResourceModel, - Data $adyenHelper, - Config $configHelper - ){ - $this->adyenPaymentResourceModel = $adyenPaymentResourceModel; - $this->adyenHelper = $adyenHelper; - $this->configHelper = $configHelper; - } + private readonly Payment $adyenPaymentResourceModel, + private readonly Data $adyenHelper + ){ } /** * Create cancel_or_refund request diff --git a/Gateway/Validator/AbstractModificationsResponseValidator.php b/Gateway/Validator/AbstractModificationsResponseValidator.php index 4f33c86a6..8d5b8a52a 100644 --- a/Gateway/Validator/AbstractModificationsResponseValidator.php +++ b/Gateway/Validator/AbstractModificationsResponseValidator.php @@ -61,53 +61,75 @@ public function validate(array $validationSubject): ResultInterface throw new ValidatorException(__("No responses were provided")); } - $isValid = true; $errorMessages = []; foreach ($responseCollection as $response) { - foreach (self::REQUIRED_RESPONSE_FIELDS as $requiredField) { - if (!array_key_exists($requiredField, $response)) { - $errorMessage = __( - '%1 field is missing in %2 response.', - $requiredField, - $this->getModificationType() - ); - - $this->adyenLogger->error($errorMessage); + $errorMessages = array_merge($errorMessages, $this->validateRequiredFields($response)); + $errorMessages = array_merge($errorMessages, $this->validateResponseStatus($response)); + } - $isValid = false; - $errorMessages[] = $errorMessage; + return $this->createResult(empty($errorMessages), $errorMessages); + } - break; - } - } + /** + * @param array $response + * @return array + */ + private function validateRequiredFields(array $response): array + { + $errorMessages = []; - if (!in_array($response['status'], $this->getValidStatuses())) { - $errorMessage = __( - 'An error occurred while validating the %1 response', + foreach (self::REQUIRED_RESPONSE_FIELDS as $requiredField) { + if (!array_key_exists($requiredField, $response)) { + $missingFieldErrorMessage = __( + '%1 field is missing in %2 response.', + $requiredField, $this->getModificationType() ); - $logMessage = sprintf( - "An error occurred while validating the %s response%s. %s", - $this->getModificationType(), - isset($response['formattedModificationAmount']) ? - ' with amount ' . - $response['formattedModificationAmount'] : '', - $response['error'] ?? '' - ); - - $this->adyenLogger->error($logMessage); + $this->adyenLogger->error($missingFieldErrorMessage); - $errorMessages[] = $errorMessage; - $isValid = false; + $errorMessages[] = $missingFieldErrorMessage; } } - return $this->createResult($isValid, $errorMessages); + return $errorMessages; + } + + /** + * @param array $response + * @return array + */ + private function validateResponseStatus(array $response): array + { + $errorMessages = []; + + if (!in_array($response['status'], $this->getValidStatuses())) { + $errorMessage = __( + 'An error occurred while validating the %1 response', + $this->getModificationType() + ); + + $logMessage = sprintf( + "An error occurred while validating the %s response%s. %s", + $this->getModificationType(), + isset($response['formattedModificationAmount']) ? + ' with amount ' . + $response['formattedModificationAmount'] : '', + $response['error'] ?? '' + ); + + $this->adyenLogger->error($logMessage); + + $errorMessages[] = $errorMessage; + } + + return $errorMessages; } /** + * Returns the modification type of the child class defined in di.xml + * * @return string */ private function getModificationType(): string @@ -116,6 +138,8 @@ private function getModificationType(): string } /** + * Returns the valid statuses of the child class defined in di.xml + * * @return array */ private function getValidStatuses(): array From f81059b060ce76c2401c7953837c82b1b14f7c91 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Wed, 12 Mar 2025 11:45:38 +0100 Subject: [PATCH 41/42] [ECP-9639] Introduce a constant for the string --- Gateway/Validator/AbstractModificationsResponseValidator.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Gateway/Validator/AbstractModificationsResponseValidator.php b/Gateway/Validator/AbstractModificationsResponseValidator.php index 8d5b8a52a..3dbd0385b 100644 --- a/Gateway/Validator/AbstractModificationsResponseValidator.php +++ b/Gateway/Validator/AbstractModificationsResponseValidator.php @@ -21,6 +21,7 @@ class AbstractModificationsResponseValidator extends AbstractValidator { + const FORMATTED_MODIFICATIONS_AMOUNT = 'formattedModificationAmount'; const REQUIRED_RESPONSE_FIELDS = [ 'status', 'pspReference' @@ -113,9 +114,9 @@ private function validateResponseStatus(array $response): array $logMessage = sprintf( "An error occurred while validating the %s response%s. %s", $this->getModificationType(), - isset($response['formattedModificationAmount']) ? + isset($response[self::FORMATTED_MODIFICATIONS_AMOUNT]) ? ' with amount ' . - $response['formattedModificationAmount'] : '', + $response[self::FORMATTED_MODIFICATIONS_AMOUNT] : '', $response['error'] ?? '' ); From 97edd2a98c9c93e2b011fd5d901aca6bedd3ffbb Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Wed, 12 Mar 2025 14:20:08 +0100 Subject: [PATCH 42/42] [ECP-9639] Refactor capture command chain --- Gateway/Http/Client/TransactionCapture.php | 149 +++++--------- Gateway/Request/CaptureDataBuilder.php | 181 +++++++++++------- .../ModificationsCapturesResponseHandler.php | 2 +- etc/di.xml | 2 - 4 files changed, 157 insertions(+), 177 deletions(-) diff --git a/Gateway/Http/Client/TransactionCapture.php b/Gateway/Http/Client/TransactionCapture.php index 05a71cbd7..d63e03074 100644 --- a/Gateway/Http/Client/TransactionCapture.php +++ b/Gateway/Http/Client/TransactionCapture.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2023 Adyen N.V. (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -14,38 +14,19 @@ use Adyen\AdyenException; use Adyen\Client; use Adyen\Model\Checkout\PaymentCaptureRequest; -use Adyen\Payment\Api\Data\OrderPaymentInterface; +use Adyen\Payment\Gateway\Validator\AbstractModificationsResponseValidator; use Adyen\Payment\Helper\Data; use Adyen\Payment\Helper\Idempotency; -use Adyen\Payment\Helper\Requests; use Adyen\Payment\Logger\AdyenLogger; -use Adyen\Service\Checkout\ModificationsApi; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Payment\Gateway\Http\ClientInterface; use Magento\Payment\Gateway\Http\TransferInterface; class TransactionCapture implements ClientInterface { - const MULTIPLE_AUTHORIZATIONS = 'multiple_authorizations'; - const FORMATTED_CAPTURE_AMOUNT = 'formatted_capture_amount'; - const CAPTURE_AMOUNT = 'capture_amount'; + const CAPTURE_AMOUNT = 'amount'; + const CAPTURE_VALUE = 'value'; const ORIGINAL_REFERENCE = 'paymentPspReference'; - const CAPTURE_RECEIVED = 'received'; - - /** - * @var Data - */ - private Data $adyenHelper; - - /** - * @var AdyenLogger - */ - private AdyenLogger $adyenLogger; - - /** - * @var Idempotency - */ - private Idempotency $idempotencyHelper; /** * @param Data $adyenHelper @@ -53,14 +34,10 @@ class TransactionCapture implements ClientInterface * @param Idempotency $idempotencyHelper */ public function __construct( - Data $adyenHelper, - AdyenLogger $adyenLogger, - Idempotency $idempotencyHelper - ) { - $this->adyenHelper = $adyenHelper; - $this->adyenLogger = $adyenLogger; - $this->idempotencyHelper = $idempotencyHelper; - } + private readonly Data $adyenHelper, + private readonly AdyenLogger $adyenLogger, + private readonly Idempotency $idempotencyHelper + ) { } /** * @param TransferInterface $transferObject @@ -70,66 +47,16 @@ public function __construct( */ public function placeRequest(TransferInterface $transferObject): array { - $request = $transferObject->getBody(); + $requestCollection = $transferObject->getBody(); $headers = $transferObject->getHeaders(); $clientConfig = $transferObject->getClientConfig(); $client = $this->adyenHelper->initializeAdyenClientWithClientConfig($clientConfig); $service = $this->adyenHelper->initializeModificationsApi($client); - $requestOptions['headers'] = $headers; - $request['applicationInfo'] = $this->adyenHelper->buildApplicationInfo($client); - - if (array_key_exists(self::MULTIPLE_AUTHORIZATIONS, $request)) { - return $this->placeMultipleCaptureRequests($service, $request, $requestOptions); - } - - $idempotencyKeyExtraData = $request['idempotencyExtraData']; - unset($request['idempotencyExtraData']); - - $idempotencyKey = $this->idempotencyHelper->generateIdempotencyKey( - $request, - $idempotencyKeyExtraData ?? null - ); - $requestOptions['idempotencyKey'] = $idempotencyKey; - - $this->adyenHelper->logRequest($request, Client::API_CHECKOUT_VERSION, '/captures'); - $paymentCaptureRequest = new PaymentCaptureRequest($request); - $responseData = []; - - try { - $response = $service->captureAuthorisedPayment( - $request['paymentPspReference'], - $paymentCaptureRequest, - $requestOptions - ); + $responseCollection = []; - $responseData = $response->toArray(); - $responseData = $this->copyParamsToResponse($responseData, $request); - $this->adyenHelper->logResponse($responseData); - } catch (AdyenException $e) { - $this->adyenHelper->logAdyenException($e); - $responseData['error'] = $e->getMessage(); - } - - return $responseData; - } - - /** - * @param ModificationsApi $service - * @param array $requestContainer - * @param array $requestOptions - * @return array - */ - private function placeMultipleCaptureRequests( - ModificationsApi $service, - array $requestContainer, - array $requestOptions - ): array { - $response = []; - $applicationInfo = $requestContainer['applicationInfo']; - - foreach ($requestContainer[self::MULTIPLE_AUTHORIZATIONS] as $request) { + foreach ($requestCollection as $request) { $idempotencyKeyExtraData = $request['idempotencyExtraData']; unset($request['idempotencyExtraData']); $idempotencyKey = $this->idempotencyHelper->generateIdempotencyKey( @@ -137,42 +64,44 @@ private function placeMultipleCaptureRequests( $idempotencyKeyExtraData ?? null ); + $request['applicationInfo'] = $this->adyenHelper->buildApplicationInfo($client); + $requestOptions['headers'] = $headers; $requestOptions['idempotencyKey'] = $idempotencyKey; + + $paymentPspReference = $request['paymentPspReference']; + unset($request['paymentPspReference']); + try { - // Copy merchant account from parent array to every request array - $request[Requests::MERCHANT_ACCOUNT] = $requestContainer[Requests::MERCHANT_ACCOUNT]; - $request['applicationInfo'] = $applicationInfo; $paymentCaptureRequest = new PaymentCaptureRequest($request); - $singleResponseObj = $service->captureAuthorisedPayment( - $request['paymentPspReference'], + $this->adyenHelper->logRequest($request, Client::API_CHECKOUT_VERSION, '/captures'); + + $response = $service->captureAuthorisedPayment( + $paymentPspReference, $paymentCaptureRequest, $requestOptions ); - $singleResponse = $singleResponseObj->toArray(); - $singleResponse[self::FORMATTED_CAPTURE_AMOUNT] = $request['amount']['currency'] . ' ' . - $this->adyenHelper->originalAmount( - $request['amount']['value'], - $request['amount']['currency'] - ); - $singleResponse = $this->copyParamsToResponse($singleResponse, $request); - $response[self::MULTIPLE_AUTHORIZATIONS][] = $singleResponse; - } catch (AdyenException $e) { - $pspReference = $request[OrderPaymentInterface::PSPREFRENCE] ?? 'pspReference not set'; + $responseData = $response->toArray(); + $this->adyenHelper->logResponse($responseData); + $responseCollection[] = $this->copyParamsToResponse($responseData, $request); + } catch (AdyenException $e) { $message = sprintf( - 'Exception occurred when attempting to capture multiple authorizations. - Authorization with pspReference %s: %s', - $pspReference, + "An error occurred during the capture attempt%s. %s", + !empty($paymentPspReference) ? + ' of authorisation with pspreference ' . $paymentPspReference : + '', $e->getMessage() ); + $responseData['error'] = $message; + $this->adyenLogger->error($message); - $response[self::MULTIPLE_AUTHORIZATIONS]['error'] = $message; + $responseCollection[] = $responseData; } } - return $response; + return $responseCollection; } /** @@ -182,8 +111,16 @@ private function placeMultipleCaptureRequests( */ private function copyParamsToResponse(array $response, array $request): array { - $response[self::CAPTURE_AMOUNT] = $request['amount']['value']; - $response[self::ORIGINAL_REFERENCE] = $request[self::ORIGINAL_REFERENCE]; + $originalAmount = $this->adyenHelper->originalAmount( + $request['amount']['value'], + $request['amount']['currency'] + ); + + $response[AbstractModificationsResponseValidator::FORMATTED_MODIFICATIONS_AMOUNT] = sprintf( + "%s %s", + $request['amount']['currency'], + $originalAmount + ); return $response; } diff --git a/Gateway/Request/CaptureDataBuilder.php b/Gateway/Request/CaptureDataBuilder.php index 0a7671b9e..a91f42e6d 100644 --- a/Gateway/Request/CaptureDataBuilder.php +++ b/Gateway/Request/CaptureDataBuilder.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2021 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -13,20 +13,19 @@ use Adyen\AdyenException; use Adyen\Payment\Api\Data\OrderPaymentInterface; -use Adyen\Payment\Gateway\Http\Client\TransactionCapture; use Adyen\Payment\Helper\AdyenOrderPayment; use Adyen\Payment\Helper\ChargedCurrency; use Adyen\Payment\Helper\Data as DataHelper; use Adyen\Payment\Helper\OpenInvoice; use Adyen\Payment\Helper\PaymentMethods; use Adyen\Payment\Logger\AdyenLogger; -use Adyen\Payment\Model\ResourceModel\Order\Payment; +use Adyen\Payment\Model\ResourceModel\Order\Payment as AdyenOrderPaymentResourceModel; use Magento\Framework\App\Action\Context; use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Data\PaymentDataObject; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; -use Magento\Sales\Model\Order; +use Magento\Payment\Model\InfoInterface; use Magento\Sales\Model\Order\Invoice; /** @@ -40,7 +39,7 @@ class CaptureDataBuilder implements BuilderInterface * @param AdyenOrderPayment $adyenOrderPaymentHelper * @param AdyenLogger $adyenLogger * @param Context $context - * @param Payment $orderPaymentResourceModel + * @param AdyenOrderPaymentResourceModel $orderPaymentResourceModel * @param OpenInvoice $openInvoiceHelper * @param PaymentMethods $paymentMethodsHelper */ @@ -50,7 +49,7 @@ public function __construct( private readonly AdyenOrderPayment $adyenOrderPaymentHelper, private readonly AdyenLogger $adyenLogger, private readonly Context $context, - private readonly Payment $orderPaymentResourceModel, + private readonly AdyenOrderPaymentResourceModel $orderPaymentResourceModel, protected readonly OpenInvoice $openInvoiceHelper, private readonly PaymentMethods $paymentMethodsHelper ) { } @@ -63,12 +62,12 @@ public function build(array $buildSubject): array /** @var PaymentDataObject $paymentDataObject */ $paymentDataObject = SubjectReader::readPayment($buildSubject); $payment = $paymentDataObject->getPayment(); - $paymentMethodInstance = $payment->getMethodInstance(); - /** @var Order $order */ $order = $payment->getOrder(); + /** @var Invoice $latestInvoice */ $latestInvoice = $order->getInvoiceCollection()->getLastItem(); $invoiceAmountCurrency = $this->chargedCurrency->getInvoiceAmountCurrency($latestInvoice); + $orderAmountCurrency = $this->chargedCurrency->getOrderAmountCurrency($order); $currency = $invoiceAmountCurrency->getCurrencyCode(); $amount = $this->adyenHelper->formatAmount($invoiceAmountCurrency->getAmount(), $currency); @@ -90,52 +89,68 @@ public function build(array $buildSubject): array throw new AdyenException($errorMessage); } - $adyenOrderPayments = $this->orderPaymentResourceModel->getLinkedAdyenOrderPayments($payment->getEntityId()); + $requestCollection = []; + $adyenOrderPayments = $this->orderPaymentResourceModel->getLinkedAdyenOrderPayments( + $payment->getEntityId() + ); + // If the full amount won't be captured OR there are multiple payments to capture if (!empty($adyenOrderPayments) && ($amount < $orderAmountCents || count($adyenOrderPayments) > 1)) { - return $this->buildPartialOrMultipleCaptureData( + $requestCollection = $this->buildPartialOrMultipleCaptureData( + $latestInvoice, $payment, + $amount, $currency, - $adyenOrderPayments, - $invoiceAmountCurrency->getAmount() + $adyenOrderPayments + ); + } else { + $requestCollection[] = $this->buildCaptureRequestPayload( + $latestInvoice, + $payment, + $amount, + $currency, + $pspReference, + $payment->getOrder()->getTotalInvoiced() ?? 0 ); } - $modificationAmount = ['value' => $amount, 'currency' => $currency]; - $requestBody = [ - "amount" => $modificationAmount, - "reference" => $payment->getOrder()->getIncrementId(), - "paymentPspReference" => $pspReference, - "idempotencyExtraData" => [ - 'totalInvoiced' => $payment->getOrder()->getTotalInvoiced() ?? 0 + return [ + 'body' => $requestCollection, + 'clientConfig' => [ + 'storeId' => $payment->getOrder()->getStoreId() ] ]; - - if ($this->paymentMethodsHelper->getRequiresLineItems($paymentMethodInstance)) { - $openInvoiceFields = $this->openInvoiceHelper->getOpenInvoiceDataForInvoice($latestInvoice); - $requestBody = array_merge($requestBody, $openInvoiceFields); - } - $request['body'] = $requestBody; - $request['clientConfig'] = ["storeId" => $payment->getOrder()->getStoreId()]; - - return $request; } /** * Return the data of the multiple capture requests required to capture the full amount OR * multiple capture requests required to capture a partial amount OR * a single capture request required to capture a partial amount + * + * @param Invoice $latestInvoice + * @param InfoInterface $payment + * @param int $captureAmountCents + * @param string $currency + * @param array $adyenOrderPayments + * @return array + * @throws LocalizedException */ - public function buildPartialOrMultipleCaptureData($payment, $currency, $adyenOrderPayments, $captureAmount): array - { - $this->adyenLogger->addAdyenInfoLog(sprintf( - 'Building PARTIAL capture request for multiple authorisations, on payment %s', - $payment->getId() - ), $this->adyenLogger->getOrderContext($payment->getOrder())); - - $captureAmountCents = $this->adyenHelper->formatAmount($captureAmount, $currency); - $paymentMethodInstance = $payment->getMethodInstance(); - $captureData = []; + private function buildPartialOrMultipleCaptureData( + Invoice $latestInvoice, + InfoInterface $payment, + int $captureAmountCents, + string $currency, + array $adyenOrderPayments + ): array { + $this->adyenLogger->addAdyenInfoLog( + sprintf( + 'Building PARTIAL capture request for multiple authorisations, on payment %s', + $payment->getId() + ), + $this->adyenLogger->getOrderContext($payment->getOrder()) + ); + + $requestCollection = []; $counterAmount = 0; $i = 0; @@ -147,6 +162,7 @@ public function buildPartialOrMultipleCaptureData($payment, $currency, $adyenOrd $paymentAmount - $totalCaptured, $currency ); + // If there is still some amount available to capture if ($availableAmountToCaptureCents > 0) { // IF the counter amount + available amount to capture from @@ -160,43 +176,72 @@ public function buildPartialOrMultipleCaptureData($payment, $currency, $adyenOrd $counterAmount += $amountCents; - $modificationAmount = [ - 'currency' => $currency, - 'value' => $amountCents - ]; - $authToCapture = [ - "amount" => $modificationAmount, - "reference" => $payment->getOrder()->getIncrementId(), - "paymentPspReference" => $adyenOrderPayment[OrderPaymentInterface::PSPREFRENCE] - ]; - - if ($this->paymentMethodsHelper->getRequiresLineItems($paymentMethodInstance)) { - $order = $payment->getOrder(); - $invoices = $order->getInvoiceCollection(); - // The latest invoice will contain only the selected items(and quantities) for the (partial) capture - /** @var Invoice $invoice */ - $invoice = $invoices->getLastItem(); - - $openInvoiceFields = $this->openInvoiceHelper->getOpenInvoiceDataForInvoice($invoice); - $authToCapture = array_merge($authToCapture, $openInvoiceFields); - } - $authToCapture['idempotencyExtraData'] = [ - 'totalInvoiced' => $adyenOrderPayment[OrderPaymentInterface::TOTAL_CAPTURED] ?? 0, - 'originalPspReference' => $adyenOrderPayment[OrderPaymentInterface::PSPREFRENCE] - ] ; + $payload = $this->buildCaptureRequestPayload( + $latestInvoice, + $payment, + $amountCents, + $currency, + $adyenOrderPayment[OrderPaymentInterface::PSPREFRENCE], + $adyenOrderPayment[OrderPaymentInterface::TOTAL_CAPTURED] ?? 0 + ); - $captureData[] = $authToCapture; + $requestCollection[] = $payload; } + $i++; } - $requestBody = [ - TransactionCapture::MULTIPLE_AUTHORIZATIONS => $captureData + return $requestCollection; + } + + /** + * @param Invoice $latestInvoice + * @param InfoInterface $payment + * @param int $amount + * @param string $currency + * @param string $pspReference + * @param float|null $totalCaptured + * @return array + * @throws LocalizedException + */ + private function buildCaptureRequestPayload( + Invoice $latestInvoice, + InfoInterface $payment, + int $amount, + string $currency, + string $pspReference, + float $totalCaptured = null + ): array { + $method = $payment->getMethod(); + $storeId = $payment->getOrder()->getStoreId(); + + if (isset($method) && $method === 'adyen_moto') { + $merchantAccount = $payment->getAdditionalInformation('motoMerchantAccount'); + } else { + $merchantAccount = $this->adyenHelper->getAdyenMerchantAccount($method, $storeId); + } + + $payload = [ + 'merchantAccount' => $merchantAccount, + 'amount' => [ + 'value' => $amount, + 'currency' => $currency + ], + 'reference' => $payment->getOrder()->getIncrementId(), + 'paymentPspReference' => $pspReference, + 'idempotencyExtraData' => [ + 'totalInvoiced' => $totalCaptured, + 'originalPspReference' => $pspReference + ] ]; - $request['body'] = $requestBody; - $request['clientConfig'] = ["storeId" => $payment->getOrder()->getStoreId()]; + // Build line items + $paymentMethodInstance = $payment->getMethodInstance(); + if ($this->paymentMethodsHelper->getRequiresLineItems($paymentMethodInstance)) { + $openInvoiceFields = $this->openInvoiceHelper->getOpenInvoiceDataForInvoice($latestInvoice); + $payload = array_merge($payload, $openInvoiceFields); + } - return $request; + return $payload; } } diff --git a/Gateway/Response/ModificationsCapturesResponseHandler.php b/Gateway/Response/ModificationsCapturesResponseHandler.php index df45aae16..5e27d9476 100644 --- a/Gateway/Response/ModificationsCapturesResponseHandler.php +++ b/Gateway/Response/ModificationsCapturesResponseHandler.php @@ -55,7 +55,7 @@ public function handle(array $handlingSubject, array $responseCollection): void $payment, $response['pspReference'] ?? '', $response[TransactionCapture::ORIGINAL_REFERENCE], - $response[TransactionCapture::CAPTURE_AMOUNT] + $response[TransactionCapture::CAPTURE_AMOUNT][TransactionCapture::CAPTURE_VALUE] ); } diff --git a/etc/di.xml b/etc/di.xml index b7ebb8764..a22690742 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1255,7 +1255,6 @@ - Adyen\Payment\Gateway\Request\MerchantAccountDataBuilder Adyen\Payment\Gateway\Request\CaptureDataBuilder Adyen\Payment\Gateway\Request\Header\HeaderDataBuilder @@ -1264,7 +1263,6 @@ - Adyen\Payment\Gateway\Request\MotoMerchantAccountDataBuilder Adyen\Payment\Gateway\Request\MotoClientConfigBuilder Adyen\Payment\Gateway\Request\CaptureDataBuilder