Skip to content
36 changes: 24 additions & 12 deletions Controller/Return/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
28 changes: 20 additions & 8 deletions Helper/Webhook/OfferClosedWebhookHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
72 changes: 50 additions & 22 deletions Test/Unit/Controller/Return/IndexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -173,27 +178,50 @@ 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
{
$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');
Expand Down
58 changes: 58 additions & 0 deletions Test/Unit/Helper/Webhook/OfferClosedWebhookHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading