diff --git a/Controller/Return/Index.php b/Controller/Return/Index.php index 52f6886e10..458488884e 100755 --- a/Controller/Return/Index.php +++ b/Controller/Return/Index.php @@ -25,7 +25,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Sales\Api\Data\OrderInterface; -use Magento\Sales\Model\Order; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\OrderFactory; use Magento\Store\Model\StoreManagerInterface; @@ -63,6 +63,7 @@ class Index extends Action * @param PaymentsDetails $paymentsDetailsHelper * @param PaymentResponseHandler $paymentResponseHandler * @param CartRepositoryInterface $cartRepository + * @param OrderRepositoryInterface $orderRepository */ public function __construct( Context $context, @@ -74,7 +75,8 @@ public function __construct( private readonly Config $configHelper, private readonly PaymentsDetails $paymentsDetailsHelper, private readonly PaymentResponseHandler $paymentResponseHandler, - private readonly CartRepositoryInterface $cartRepository + private readonly CartRepositoryInterface $cartRepository, + private readonly OrderRepositoryInterface $orderRepository ) { parent::__construct($context); } @@ -168,18 +170,28 @@ protected function validateRedirectResponse(array $redirectResponse): bool /** * @throws LocalizedException */ - private function getOrder(?string $incrementId = null): Order + private function getOrder(?string $incrementId = null): OrderInterface { - if ($incrementId !== null) { - $order = $this->orderFactory->create()->loadByIncrementId($incrementId); - } else { - $order = $this->session->getLastRealOrder(); - } + try { + if ($incrementId !== null) { + $entity = $this->orderFactory->create()->loadByIncrementId($incrementId); + + if (!$entity->getEntityId()) { + throw new NoSuchEntityException( + __("The entity that was requested doesn't exist. Verify the entity and try again.") + ); + } - if (!$order->getId()) { - throw new LocalizedException( - __('Order cannot be loaded') - ); + $order = $this->orderRepository->get($entity->getEntityId()); + } else { + $order = $this->session->getLastRealOrder(); + + if (!$order->getId()) { + throw new NoSuchEntityException(); + } + } + } catch (NoSuchEntityException $e) { + throw new LocalizedException(__('Order cannot be loaded')); } return $order; diff --git a/Helper/Webhook/OfferClosedWebhookHandler.php b/Helper/Webhook/OfferClosedWebhookHandler.php index db1f3e0547..196700ec48 100644 --- a/Helper/Webhook/OfferClosedWebhookHandler.php +++ b/Helper/Webhook/OfferClosedWebhookHandler.php @@ -96,19 +96,31 @@ public function handleWebhook(MagentoOrder $order, Notification $notification, s return $order; } - // Move the order from PAYMENT_REVIEW to NEW, so that it can be cancelled - if (!$order->isCanceled() - && !$order->canCancel() - && $this->configHelper->getNotificationsCanCancel($order->getStoreId()) - ) { - $order->setState(MagentoOrder::STATE_NEW); + if ($order->isCanceled()) { + $message = __('The order has already been cancelled. Skipping the %1 webhook.', + $notification->getEventCode()); + + $this->adyenLogger->addAdyenNotification($message, [ + 'pspReference' => $notification->getPspreference(), + 'merchantReference' => $notification->getMerchantReference() + ]); + + $order->addCommentToStatusHistory($message); + } else { + // Move the order from PAYMENT_REVIEW to NEW, so that it can be cancelled + if (!$order->isCanceled() + && !$order->canCancel() + && $this->configHelper->getNotificationsCanCancel($order->getStoreId()) + ) { + $order->setState(MagentoOrder::STATE_NEW); + } + + $this->orderHelper->holdCancelOrder($order, true); } // Clean-up the data temporarily stored in `additional_information` $this->cleanupAdditionalInformation->execute($order->getPayment()); - $this->orderHelper->holdCancelOrder($order, true); - return $order; } } diff --git a/Test/Unit/Controller/Return/IndexTest.php b/Test/Unit/Controller/Return/IndexTest.php index ccc114fe14..a188ad67ff 100644 --- a/Test/Unit/Controller/Return/IndexTest.php +++ b/Test/Unit/Controller/Return/IndexTest.php @@ -10,23 +10,24 @@ use Adyen\Payment\Helper\PaymentsDetails; use Adyen\Payment\Helper\Quote; use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Sales\OrderRepository; use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; use Exception; use Magento\Checkout\Model\Session; use Magento\Framework\App\Action\Context; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\App\Response\RedirectInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\Quote as QuoteModel; -use Magento\Sales\Api\OrderRepositoryInterface; -use Magento\Sales\Model\Order; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order as OrderModel; use Magento\Sales\Model\OrderFactory; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; class IndexTest extends AbstractAdyenTestCase { @@ -59,7 +60,7 @@ protected function setUp(): void $this->paymentsDetailsHelper = $this->createMock(PaymentsDetails::class); $this->paymentResponseHandler = $this->createMock(PaymentResponseHandler::class); $this->cartRepository = $this->createMock(CartRepositoryInterface::class); - $this->orderRepository = $this->createMock(OrderRepositoryInterface::class); + $this->orderRepository = $this->createMock(OrderRepository::class); $this->request = $this->createMock(RequestInterface::class); $this->response = $this->createMock(RedirectInterface::class); @@ -107,15 +108,17 @@ public function testExecuteWithSuccessfulRedirect(): void $quote->expects($this->once())->method('setIsActive')->with(false); $this->session->method('getQuote')->willReturn($quote); - $order = $this->createMock(Order::class); - $order->method('getId')->willReturn(1); + $order = $this->createMock(OrderInterface::class); $order->method('getIncrementId')->willReturn('1001'); - $order->method('getPayment')->willReturn($this->createMock(Order\Payment::class)); + $order->method('getPayment')->willReturn($this->createMock(OrderModel\Payment::class)); + $order->method('getEntityId')->willReturn(1); + + $orderModel = $this->createMock(OrderModel::class); + $orderModel->method('getEntityId')->willReturn(1); + $orderModel->method('loadByIncrementId')->willReturn($orderModel); - $orderModel = $this->createMock(Order::class); - $orderModel->method('loadByIncrementId')->willReturn($order); - $orderModel->method('getId')->willReturn(1); $this->orderFactory->method('create')->willReturn($orderModel); + $this->orderRepository->method('get')->willReturn($order); $this->paymentsDetailsHelper->method('initiatePaymentDetails')->willReturn(['resultCode' => 'Authorised']); $this->paymentResponseHandler->method('handlePaymentsDetailsResponse')->willReturn(true); @@ -139,12 +142,14 @@ public function testExecuteWithFailedRedirect(): void ['return_path', 1, 'checkout/cart'] ]); - $order = $this->createMock(Order::class); - $order->method('getId')->willReturn(1); + $order = $this->createMock(OrderInterface::class); + + $orderModel = $this->createMock(OrderModel::class); + $orderModel->method('getEntityId')->willReturn(1); + $orderModel->method('loadByIncrementId')->willReturn($orderModel); - $orderModel = $this->createMock(Order::class); - $orderModel->method('loadByIncrementId')->willReturn($order); $this->orderFactory->method('create')->willReturn($orderModel); + $this->orderRepository->method('get')->willReturn($order); $this->paymentsDetailsHelper->method('initiatePaymentDetails')->willThrowException(new Exception('Invalid')); $this->paymentResponseHandler->method('handlePaymentsDetailsResponse')->willReturn(false); @@ -173,16 +178,22 @@ public function testExecuteWithoutParams(): void public function testGetOrderWithValidId(): void { - $order = $this->createMock(Order::class); - $order->method('getId')->willReturn(10); - $this->orderFactory->method('create')->willReturn($order); - $order->method('loadByIncrementId')->with('1001')->willReturn($order); + $orderModel = $this->createMock(OrderModel::class); + $orderModel->method('getEntityId')->willReturn(10); + $orderModel->method('loadByIncrementId')->willReturn($orderModel); + + $this->orderFactory->method('create')->willReturn($orderModel); + + $order = $this->createMock(OrderInterface::class); + $order->method('getEntityId')->willReturn(10); + + $this->orderRepository->method('get')->with(10)->willReturn($order); $reflection = new \ReflectionClass(Index::class); $method = $reflection->getMethod('getOrder'); $method->setAccessible(true); $result = $method->invokeArgs($this->indexController, ['1001']); - $this->assertSame($order, $result); + $this->assertInstanceOf(OrderInterface::class, $result); } public function testGetOrderThrowsExceptionOnInvalidOrder(): void @@ -190,10 +201,27 @@ public function testGetOrderThrowsExceptionOnInvalidOrder(): void $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Order cannot be loaded'); - $order = $this->createMock(Order::class); + $order = $this->createMock(OrderModel::class); $order->method('getId')->willReturn(null); - $this->orderFactory->method('create')->willReturn($order); - $order->method('loadByIncrementId')->willReturn($order); + $this->session->method('getLastRealOrder')->willReturn($order); + + $reflection = new \ReflectionClass(Index::class); + $method = $reflection->getMethod('getOrder'); + $method->setAccessible(true); + $method->invokeArgs($this->indexController, [null]); + + } + + public function testGetOrderDoesNotTranslateIntoAnOrderWithValidIncrementId(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Order cannot be loaded'); + + $orderModel = $this->createMock(OrderModel::class); + $orderModel->method('loadByIncrementId')->willReturnSelf(); + $orderModel->method('getEntityId')->willReturn(null); + + $this->orderFactory->method('create')->willReturn($orderModel); $reflection = new \ReflectionClass(Index::class); $method = $reflection->getMethod('getOrder'); diff --git a/Test/Unit/Helper/Webhook/OfferClosedWebhookHandlerTest.php b/Test/Unit/Helper/Webhook/OfferClosedWebhookHandlerTest.php index c3d7f40d09..71df474b90 100644 --- a/Test/Unit/Helper/Webhook/OfferClosedWebhookHandlerTest.php +++ b/Test/Unit/Helper/Webhook/OfferClosedWebhookHandlerTest.php @@ -116,6 +116,64 @@ public function testHandleWebhookReturnsOrderWhenCapturedPaymentsExist() $this->assertEquals($order, $result); } + public function testHandleWebhookSkipsWhenOrderAlreadyCancelled() + { + // Create a sample MagentoOrder and Notification + $order = $this->createMock(MagentoOrder::class); + $payment = $this->createPartialMock(Payment::class, ['getMethod', 'getEntityId']); + $notification = $this->createMock(Notification::class); + + // Set up payment mock + $payment->method('getMethod')->willReturn('adyen_cc'); + $payment->method('getEntityId')->willReturn(123); + $order->method('getPayment')->willReturn($payment); + + // Mock order as already cancelled + $order->method('isCanceled')->willReturn(true); + + // Mock notification methods for logging + $notification->method('getPspreference')->willReturn('test_psp_reference'); + $notification->method('getMerchantReference')->willReturn('test_merchant_reference'); + $notification->method('getEventCode')->willReturn('OFFER_CLOSED'); + + // Mock payment method comparison to return true (so we reach the isCanceled check) + $this->paymentMethodsHelper->method('compareOrderAndWebhookPaymentMethods') + ->with($order, $notification) + ->willReturn(true); + + // Mock empty captured payments (so we reach the isCanceled check) + $this->orderPaymentResourceModel->method('getLinkedAdyenOrderPayments')->willReturn([]); + + // Expect addCommentToStatusHistory to be called with the skip message + $order->expects($this->once()) + ->method('addCommentToStatusHistory'); + + // Create mock for logger to verify it's called + $mockAdyenLogger = $this->createMock(AdyenLogger::class); + $mockAdyenLogger->expects($this->once()) + ->method('addAdyenNotification'); + + // Create mock for cleanup to verify it's called + $cleanupAdditionalInformation = $this->createMock(CleanupAdditionalInformation::class); + $cleanupAdditionalInformation->expects($this->once()) + ->method('execute') + ->with($payment); + + // Create an instance of the OfferClosedWebhookHandler + $webhookHandler = $this->createOfferClosedWebhookHandler( + $this->paymentMethodsHelper, + $mockAdyenLogger, + null, + null, + $this->orderPaymentResourceModel, + $cleanupAdditionalInformation + ); + + // Call the handleWebhook method and assert that it returns the order + $result = $webhookHandler->handleWebhook($order, $notification, 'PAYMENT_REVIEW'); + $this->assertEquals($order, $result); + } + protected function createOfferClosedWebhookHandler( $mockPaymentMethodsHelper = null, $mockAdyenLogger = null,