Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECP-9431] Handling Refund Delay and Order Cancellation Issues when using Giftcard + redirected Payment method #2771

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a425b70
Handling Refund Delay and Order Cancellation Issues when using Gift C…
khushboo-singhvi Oct 15, 2024
fc12873
solving code sniffer issues
khushboo-singhvi Oct 16, 2024
8c7ea50
Updating unit test
khushboo-singhvi Oct 17, 2024
3f921bc
Updating the composer version from V1 to 2 to solve the failing pipel…
khushboo-singhvi Oct 17, 2024
60b0bae
Updating config to allow plugins
khushboo-singhvi Oct 17, 2024
a59d157
Updating unit test
khushboo-singhvi Oct 17, 2024
37772dc
Updating unit test
khushboo-singhvi Oct 17, 2024
569ccf8
Updating unit test
khushboo-singhvi Oct 18, 2024
85f37c3
Updating unit test
khushboo-singhvi Oct 18, 2024
ec5f014
Updating unit test for asserting return from private methods
khushboo-singhvi Oct 18, 2024
ad67f20
Updating unit test for asserting return from private methods
khushboo-singhvi Oct 18, 2024
f28a408
Updating unit test for asserting return from private methods
khushboo-singhvi Oct 21, 2024
7214dd2
Updating unit test for asserting return from private methods
khushboo-singhvi Oct 22, 2024
416e397
Updating unit test for asserting return from private methods
khushboo-singhvi Oct 22, 2024
f5b530d
Updating unit test for asserting return from private methods
khushboo-singhvi Oct 23, 2024
0b99a02
Updating unit test for asserting return from private methods
khushboo-singhvi Oct 23, 2024
b522bf9
Adding more assertions
khushboo-singhvi Oct 23, 2024
ea7760b
Merge branch 'main' into ECP-9431
khushboo-singhvi Oct 23, 2024
e2a23a9
Adding more assertions
khushboo-singhvi Oct 23, 2024
7c409a7
Merge remote-tracking branch 'origin/ECP-9431' into ECP-9431
khushboo-singhvi Oct 23, 2024
258f342
Updating logic of canceling the already authorised the giftcards orders
khushboo-singhvi Oct 23, 2024
5e9244c
Merge branch 'main' into ECP-9431
khushboo-singhvi Oct 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
79 changes: 78 additions & 1 deletion Helper/PaymentResponseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
]);
}
}
}
}
}
150 changes: 145 additions & 5 deletions Test/Unit/Helper/PaymentResponseHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand All @@ -43,7 +46,6 @@ class PaymentResponseHandlerTest extends AbstractAdyenTestCase
private $orderRepositoryMock;
private $orderHistoryFactoryMock;
private $stateDataHelperMock;

private $paymentResponseHandler;

protected function setUp(): void
Expand All @@ -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();
Expand All @@ -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,
Expand All @@ -85,7 +92,9 @@ protected function setUp(): void
$this->orderHelperMock,
$this->orderRepositoryMock,
$this->orderHistoryFactoryMock,
$this->stateDataHelperMock
$this->stateDataHelperMock,
$this->paymentResponseCollectionFactoryMock,
$this->configHelperMock
);
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
}
6 changes: 6 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Loading