diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 462312b47..de79e4ac3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - tools: composer:v1 + tools: composer:2 - name: Test plugin installation run: | diff --git a/Helper/PaymentResponseHandler.php b/Helper/PaymentResponseHandler.php index dd5950ff6..7529379a5 100644 --- a/Helper/PaymentResponseHandler.php +++ b/Helper/PaymentResponseHandler.php @@ -11,7 +11,10 @@ namespace Adyen\Payment\Helper; +use Adyen\Model\Checkout\CancelOrderRequest; +use Adyen\Payment\Helper\Config as Config; use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\ResourceModel\PaymentResponse\CollectionFactory as PaymentResponseCollectionFactory; use Exception; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\InputException; @@ -21,6 +24,9 @@ use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\ResourceModel\Order; use Magento\Sales\Model\Order as OrderModel; +use Adyen\Payment\Helper\Data as Data; +use Magento\Framework\Mail\Exception\InvalidArgumentException; +use Adyen\Client; class PaymentResponseHandler { @@ -57,6 +63,8 @@ class PaymentResponseHandler private OrderRepository $orderRepository; private HistoryFactory $orderHistoryFactory; private StateData $stateDataHelper; + private PaymentResponseCollectionFactory $paymentResponseCollectionFactory; + private Config $configHelper; public function __construct( AdyenLogger $adyenLogger, @@ -67,7 +75,9 @@ public function __construct( \Adyen\Payment\Helper\Order $orderHelper, OrderRepository $orderRepository, HistoryFactory $orderHistoryFactory, - StateData $stateDataHelper + StateData $stateDataHelper, + PaymentResponseCollectionFactory $paymentResponseCollectionFactory, + Config $configHelper ) { $this->adyenLogger = $adyenLogger; $this->vaultHelper = $vaultHelper; @@ -78,6 +88,8 @@ public function __construct( $this->orderRepository = $orderRepository; $this->orderHistoryFactory = $orderHistoryFactory; $this->stateDataHelper = $stateDataHelper; + $this->paymentResponseCollectionFactory = $paymentResponseCollectionFactory; + $this->configHelper = $configHelper; } public function formatPaymentResponse( @@ -279,6 +291,10 @@ public function handlePaymentsDetailsResponse( break; case self::REFUSED: case self::CANCELLED: + $this->hasActiveGiftCardPayments( + $paymentsDetailsResponse['merchantReference'], $order + ); + // Cancel order in case result is refused if (null !== $order) { // Check if the current state allows for changing to new for cancellation @@ -341,4 +357,65 @@ private function isValidMerchantReference(array $paymentsDetailsResponse, OrderI return true; } + + // Method to check for existing Gift Card payments + private function hasActiveGiftCardPayments($merchantReference, $order) + { + $paymentResponseCollection = $this->paymentResponseCollectionFactory->create() + ->addFieldToFilter('merchant_reference', $merchantReference) + ->addFieldToFilter('result_code', 'Authorised'); + + if ($paymentResponseCollection->getSize() > 0) { + $getGiftcardDetails = $paymentResponseCollection->getData(); + + //Cancel the Authorised Payments + $storeId = $order->getStoreId(); + $client = $this->dataHelper->initializeAdyenClient($storeId); + $service = $this->dataHelper->initializeOrdersApi($client); + foreach ($getGiftcardDetails as $giftcardData) { + try { + // Decode JSON response and validate it + $response = json_decode($giftcardData['response'], true); + if (json_last_error() !== JSON_ERROR_NONE || !isset($response['order'])) { + throw new InvalidArgumentException('Invalid giftcard response data'); + } + + // Extract order data and PSPRef + $orderData = $response['order']['orderData'] ?? null; + $pspReference = $response['order']['pspReference'] ?? null; + + if (!$orderData || !$pspReference) { + throw new InvalidArgumentException('Missing orderData or pspReference in the response'); + } + + // Prepare cancel request + $merchantAccount = $this->configHelper->getAdyenAbstractConfigData("merchant_account", $storeId); + $cancelRequest = [ + 'order' => [ + 'pspReference' => $pspReference, + 'orderData' => $orderData, + ], + 'merchantAccount' => $merchantAccount, + ]; + $this->dataHelper->logRequest($cancelRequest, Client::API_CHECKOUT_VERSION, '/orders/cancel'); + // Call the cancel service + $cancelResponse = $service->cancelOrder(new CancelOrderRequest($cancelRequest)); + $response = $cancelResponse->toArray(); + $this->dataHelper->logResponse($response); + if (is_null($response['resultCode'])) { + // In case the result is unknown we log the request and don't update the history + $this->adyenLogger->error( + "Unexpected result query parameter for cancel order request. Response: " . json_encode($response) + ); + } + } catch (\Exception $e) { + // Log the error with relevant information for debugging + $this->adyenLogger->error('Error canceling partial payments', [ + 'exception' => $e->getMessage(), + 'giftcardData' => $giftcardData, + ]); + } + } + } + } } diff --git a/Test/Unit/Helper/PaymentResponseHandlerTest.php b/Test/Unit/Helper/PaymentResponseHandlerTest.php index 6f88669d5..71ac77bb8 100644 --- a/Test/Unit/Helper/PaymentResponseHandlerTest.php +++ b/Test/Unit/Helper/PaymentResponseHandlerTest.php @@ -10,8 +10,7 @@ */ namespace Adyen\Payment\Test\Unit\Helper; -namespace Adyen\Payment\Test\Unit\Helper; - +use Adyen\Client; use Adyen\Payment\Helper\PaymentResponseHandler; use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Helper\Vault; @@ -29,6 +28,10 @@ use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\Order\Status\HistoryFactory; use Adyen\Payment\Helper\StateData; +use Adyen\Payment\Model\ResourceModel\PaymentResponse\Collection; +use Adyen\Payment\Model\ResourceModel\PaymentResponse\CollectionFactory; +use Adyen\Payment\Helper\Config as Config; +use ReflectionClass; class PaymentResponseHandlerTest extends AbstractAdyenTestCase { @@ -43,7 +46,6 @@ class PaymentResponseHandlerTest extends AbstractAdyenTestCase private $orderRepositoryMock; private $orderHistoryFactoryMock; private $stateDataHelperMock; - private $paymentResponseHandler; protected function setUp(): void @@ -61,6 +63,11 @@ protected function setUp(): void 'create' ]); $this->stateDataHelperMock = $this->createMock(StateData::class); + $this->configHelperMock = $this->createMock(Config::class); + + $this->paymentResponseMockForFactory = $this->createMock(Collection::class); + + $this->paymentResponseCollectionFactoryMock = $this->createGeneratedMock(CollectionFactory::class, ['create']); $orderHistory = $this->createMock(History::class); $orderHistory->method('setStatus')->willReturnSelf(); @@ -74,7 +81,7 @@ protected function setUp(): void $this->orderMock->method('getStatus')->willReturn('pending'); $this->orderMock->method('getIncrementId')->willReturn('00123456'); - $this->orderHelperMock->method('setStatusOrderCreation')->willReturn( $this->orderMock); + $this->orderHelperMock->method('setStatusOrderCreation')->willReturn($this->orderMock); $this->paymentResponseHandler = new PaymentResponseHandler( $this->adyenLoggerMock, @@ -85,7 +92,9 @@ protected function setUp(): void $this->orderHelperMock, $this->orderRepositoryMock, $this->orderHistoryFactoryMock, - $this->stateDataHelperMock + $this->stateDataHelperMock, + $this->paymentResponseCollectionFactoryMock, + $this->configHelperMock ); } @@ -399,6 +408,65 @@ public function testHandlePaymentsDetailsResponseCancelOrRefused($resultCode) ] ]; + $this->paymentResponseMockForFactory->expects($this->any()) + ->method('addFieldToFilter') + ->willReturn($this->paymentResponseMockForFactory); + + $this->paymentResponseMockForFactory->expects($this->any()) + ->method('getSize') + ->willReturn(1); // Simulate there is at least one record + + // Mock getData to return the desired array of data from the database + $this->paymentResponseMockForFactory->expects($this->any()) + ->method('getData') + ->willReturn([ + [ + 'merchant_reference' => '12345', + 'result_code' => 'Authorised', + 'response' => '{ + "additionalData":{"paymentMethod":"svs","merchantReference":"123","acquirerCode":"Test"}, + "amount":{"currency":"EUR","value":5000}, + "merchantReference":"123", + "order":{"amount":{"currency":"EUR","value":17800},"expiresAt":"2024-10-10T13:11:37Z", + "orderData":"orderData....", + "pspReference":"XYZ654", + "reference":"123", + "remainingAmount":{"currency":"EUR","value":12800}}, + "paymentMethod":{"brand":"svs","type":"giftcard"}, + "pspReference":"ABC123","resultCode":"Authorised"}' + ] + ]); + + $this->paymentResponseCollectionFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->paymentResponseMockForFactory); + + $merchantAccount = 'mock_merchant_account'; + $storeId = 1; + $this->orderMock->expects($this->once())->method('getStoreId')->willReturn($storeId); + $this->configHelperMock->expects($this->any()) + ->method('getAdyenAbstractConfigData') + ->with('merchant_account', $storeId) + ->willReturn($merchantAccount); + + // Create an instance of the class that has the private method + $class = new \ReflectionClass(PaymentResponseHandler::class); + $instance = $class->newInstanceWithoutConstructor(); + + // Inject the mocked factory into the instance if necessary + $property = $class->getProperty('paymentResponseCollectionFactory'); + $property->setAccessible(true); + $property->setValue($instance, $this->paymentResponseCollectionFactoryMock); + + // Use Reflection to access the private method + $method = $class->getMethod('hasActiveGiftCardPayments'); + $method->setAccessible(true); + + // Mock order cancellation + $this->orderMock->expects($this->any()) + ->method('canCancel') + ->willReturn(true); + $this->adyenLoggerMock->expects($this->atLeastOnce())->method('addAdyenResult'); $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse( @@ -455,4 +523,76 @@ public function testHandlePaymentsDetailsResponseInvalidMerchantReference(){ $this->assertFalse($result); } + + public function testHandlePaymentsDetailsResponseValidMerchantReference() + { + $paymentsDetailsResponse = [ + 'resultCode' => PaymentResponseHandler::AUTHORISED, + 'pspReference' => 'ABC123456789', + 'paymentMethod' => [ + 'brand' => 'ideal' + ], + 'merchantReference' => '00123456' // assuming this is a valid reference + ]; + // Mock the isValidMerchantReference to return true + $reflectionClass = new ReflectionClass(PaymentResponseHandler::class); + $method = $reflectionClass->getMethod('isValidMerchantReference'); + $method->setAccessible(true); + $isValidMerchantReference = $method->invokeArgs($this->paymentResponseHandler, [$paymentsDetailsResponse,$this->orderMock]); + $this->assertTrue($isValidMerchantReference); + } + + public function testPaymentDetailsCallFailureLogsError() + { + $resultCode = 'some_result_code'; + $paymentsDetailsResponse = ['error' => 'some error message']; + + // Expect the logger to be called with the specific message + $this->adyenLoggerMock->expects($this->once()) + ->method('error'); + + // Call the method that triggers the logging, e.g., handlePaymentDetailsFailure() + $this->paymentResponseHandler->handlePaymentsDetailsResponse( + $paymentsDetailsResponse, + $this->orderMock + ); + } + + public function testLogsErrorAndReturnsFalseForUnknownResult() + { + // Arrange + $paymentsDetailsResponse = [ + 'merchantReference' => '00123456' + ]; + + // Mock the logger to expect an error to be logged + $this->adyenLoggerMock->expects($this->once()) + ->method('error') + ->with($this->stringContains('Unexpected result query parameter. Response: ' . json_encode($paymentsDetailsResponse))); + + // Act: Call the method that will trigger the unexpected result handling + $result = $this->paymentResponseHandler->handlePaymentsDetailsResponse($paymentsDetailsResponse, $this->orderMock); + + // Assert: Ensure the method returned false + $this->assertFalse($result); + } + + public function testOrderStatusUpdateWhenResponseIsValid() + { + $paymentsDetailsResponse = [ + 'merchantReference' => '00123456', + 'resultCode' => 'AUTHORISED' + ]; + + $this->orderMock->expects($this->once()) + ->method('getState') + ->willReturn('pending_payment'); + + // Mock the order repository to save the order + $this->orderRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->orderMock); + + $this->paymentResponseHandler->handlePaymentsDetailsResponse($paymentsDetailsResponse, $this->orderMock); + } } diff --git a/composer.json b/composer.json index 3f6ebb8cd..cc9952d42 100755 --- a/composer.json +++ b/composer.json @@ -43,5 +43,11 @@ "Composer\\Config::disableProcessTimeout", "vendor/bin/phpunit -c Test/phpunit.xml" ] + }, + "config": { + "allow-plugins": { + "magento/composer-dependency-version-audit-plugin": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } } }