diff --git a/Gateway/Request/PaymentDataBuilder.php b/Gateway/Request/PaymentDataBuilder.php index f899d3d71..ac77ffdeb 100644 --- a/Gateway/Request/PaymentDataBuilder.php +++ b/Gateway/Request/PaymentDataBuilder.php @@ -14,6 +14,7 @@ use Adyen\Exception\MissingDataException; use Adyen\Payment\Helper\ChargedCurrency; use Adyen\Payment\Helper\Requests; +use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Data\PaymentDataObject; use Magento\Payment\Gateway\Helper\SubjectReader; @@ -31,18 +32,26 @@ class PaymentDataBuilder implements BuilderInterface */ private $chargedCurrency; + /** + * @var CheckoutSession + */ + private CheckoutSession $checkoutSession; + /** * PaymentDataBuilder constructor. * * @param Requests $adyenRequestsHelper * @param ChargedCurrency $chargedCurrency + * @param CheckoutSession $checkoutSession */ public function __construct( Requests $adyenRequestsHelper, - ChargedCurrency $chargedCurrency + ChargedCurrency $chargedCurrency, + CheckoutSession $checkoutSession ) { $this->adyenRequestsHelper = $adyenRequestsHelper; $this->chargedCurrency = $chargedCurrency; + $this->checkoutSession = $checkoutSession; } /** @@ -55,20 +64,23 @@ public function build(array $buildSubject): array { /** @var PaymentDataObject $paymentDataObject */ $paymentDataObject = SubjectReader::readPayment($buildSubject); - - $order = $paymentDataObject->getOrder(); $payment = $paymentDataObject->getPayment(); $fullOrder = $payment->getOrder(); - + $quote = $this->checkoutSession->getQuote(); $amountCurrency = $this->chargedCurrency->getOrderAmountCurrency($fullOrder); $currencyCode = $amountCurrency->getCurrencyCode(); $amount = $amountCurrency->getAmount(); - $reference = $order->getOrderIncrementId(); + $reference = $fullOrder->getIncrementId(); + + $shopperConversionIdData = $quote->getPayment()->getAdditionalInformation('shopper_conversion_id'); + + $shopperConversionId = !empty($shopperConversionIdData) ? (string) json_decode($shopperConversionIdData, true) : ''; $request['body'] = $this->adyenRequestsHelper->buildPaymentData( $amount, $currencyCode, $reference, + $shopperConversionId, [] ); diff --git a/Helper/GenerateShopperConversionId.php b/Helper/GenerateShopperConversionId.php new file mode 100644 index 000000000..6948f1a2d --- /dev/null +++ b/Helper/GenerateShopperConversionId.php @@ -0,0 +1,52 @@ +checkoutSession = $checkoutSession; + } + + /** + * Get or generate a ShopperConversionID + * + * @return string + */ + public function getShopperConversionId(): string + { + $shopperConversionId = Uuid::generateV4(); + + $quote = $this->checkoutSession->getQuote(); + $payment = $quote->getPayment(); + + // Store shopperConversionId in additional information + $payment->setAdditionalInformation(self::SHOPPER_CONVERSION_ID, json_encode($shopperConversionId)); + + // Save the quote to persist additional_information + $quote->setPayment($payment); + $quote->save(); + + return $shopperConversionId; + } +} diff --git a/Helper/PaymentMethods.php b/Helper/PaymentMethods.php index ee1f0f283..353bb2622 100644 --- a/Helper/PaymentMethods.php +++ b/Helper/PaymentMethods.php @@ -46,6 +46,7 @@ use Magento\Store\Model\Store; use Magento\Vault\Api\PaymentTokenRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Checkout\Model\Session as CheckoutSession; class PaymentMethods extends AbstractHelper { @@ -171,6 +172,16 @@ class PaymentMethods extends AbstractHelper */ private SearchCriteriaBuilder $searchCriteriaBuilder; + /** + * @var GenerateShopperConversionId + */ + private GenerateShopperConversionId $generateShopperConversionId; + + /** + * @var CheckoutSession + */ + private CheckoutSession $checkoutSession; + /** * @param Context $context * @param CartRepositoryInterface $quoteRepository @@ -190,6 +201,8 @@ class PaymentMethods extends AbstractHelper * @param Data $adyenDataHelper * @param PaymentTokenRepositoryInterface $paymentTokenRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param GenerateShopperConversionId $generateShopperConversionId + * @param CheckoutSession $checkoutSession */ public function __construct( Context $context, @@ -209,7 +222,9 @@ public function __construct( SerializerInterface $serializer, AdyenDataHelper $adyenDataHelper, PaymentTokenRepositoryInterface $paymentTokenRepository, - SearchCriteriaBuilder $searchCriteriaBuilder + SearchCriteriaBuilder $searchCriteriaBuilder, + GenerateShopperConversionId $generateShopperConversionId, + CheckoutSession $checkoutSession, ) { parent::__construct($context); $this->quoteRepository = $quoteRepository; @@ -229,6 +244,8 @@ public function __construct( $this->adyenDataHelper = $adyenDataHelper; $this->paymentTokenRepository = $paymentTokenRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->generateShopperConversionId = $generateShopperConversionId; + $this->checkoutSession = $checkoutSession; } /** @@ -581,6 +598,18 @@ protected function getPaymentMethodsRequest( $this->adyenDataHelper->padShopperReference($this->getCurrentShopperReference()); } + $quote = $this->checkoutSession->getQuote(); + + $shopperConversionIdData = $quote->getPayment()->getAdditionalInformation('shopper_conversion_id'); + + $shopperConversionId = !empty($shopperConversionIdData) ? (string) json_decode($shopperConversionIdData, true) : ''; + + if(!empty($shopperConversionId)) { + $shopperConversionId = $this->generateShopperConversionId->getShopperConversionId(); + $paymentMethodRequest["shopperConversionId"] = $shopperConversionId; + } + + $amountValue = $this->adyenHelper->formatAmount($this->getCurrentPaymentAmount(), $currencyCode); if (!empty($amountValue)) { diff --git a/Helper/Requests.php b/Helper/Requests.php index 0d6594d94..62f201129 100644 --- a/Helper/Requests.php +++ b/Helper/Requests.php @@ -14,7 +14,7 @@ use Adyen\Payment\Model\Config\Source\CcType; use Adyen\Payment\Model\Ui\AdyenCcConfigProvider; use Adyen\Payment\Model\Ui\AdyenPayByLinkConfigProvider; -use Adyen\Util\Uuid; +use Adyen\Payment\Helper\Util\Uuid; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Request\Http as Http; @@ -276,20 +276,23 @@ public function buildAddressData($billingAddress, $shippingAddress, $storeId, $r } /** - * @param array $request * @param $amount * @param $currencyCode * @param $reference + * @param $shopperConversionId + * @param array $request * @return array */ - public function buildPaymentData($amount, $currencyCode, $reference, array $request = []) + public function buildPaymentData($amount, $currencyCode, $reference, $shopperConversionId,array $request = []): array { + var_dump($amount); $request['amount'] = [ 'currency' => $currencyCode, 'value' => $this->adyenHelper->formatAmount($amount, $currencyCode) ]; $request["reference"] = $reference; + $request["shopperConversionId"] = $shopperConversionId; return $request; } diff --git a/Helper/Util/Uuid.php b/Helper/Util/Uuid.php new file mode 100644 index 000000000..45f3fa066 --- /dev/null +++ b/Helper/Util/Uuid.php @@ -0,0 +1,49 @@ +getMessage(), 0, $e); + } + + // Set the version to 4 (0100) + $random[6] = chr((ord($random[6]) & 0x0f) | 0x40); + + // Set the variant to RFC 4122 (10xx) + $random[8] = chr((ord($random[8]) & 0x3f) | 0x80); + + // Convert binary to hexadecimal and format as UUID + return sprintf( + '%08s-%04s-%04s-%04s-%12s', + bin2hex(substr($random, 0, 4)), + bin2hex(substr($random, 4, 2)), + bin2hex(substr($random, 6, 2)), + bin2hex(substr($random, 8, 2)), + bin2hex(substr($random, 10, 6)) + ); + } +} diff --git a/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php b/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php new file mode 100644 index 000000000..93f95c673 --- /dev/null +++ b/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php @@ -0,0 +1,145 @@ +adyenRequestsHelperMock = $this->createMock(Requests::class); + $this->chargedCurrencyMock = $this->createMock(ChargedCurrency::class); + $this->checkoutSessionMock = $this->createMock(CheckoutSession::class); + + $this->paymentDataBuilder = new PaymentDataBuilder( + $this->adyenRequestsHelperMock, + $this->chargedCurrencyMock, + $this->checkoutSessionMock + ); + + // Mock PaymentDataObject + $this->paymentDataObjectMock = $this->createMock(PaymentDataObject::class); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentMock = $this->createMock(InfoInterface::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->quotePaymentMock = $this->createMock(Payment::class); + } + + /** + * Tear down the test environment + */ + protected function tearDown(): void + { + $this->paymentDataBuilder = null; + parent::tearDown(); + } + + /** + * Test build() method + * + * @throws MissingDataException + * @throws LocalizedException + */ + public function testBuild(): void + { + $mockCurrencyCode = 'EUR'; + $mockAmount = 100.00; + $mockReference = '000000123'; + + // Mock CheckoutSession interaction + $this->checkoutSessionMock->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteMock); + + // Mock Quote Payment Additional Information + $this->quoteMock->expects($this->once()) + ->method('getPayment') + ->willReturn($this->quotePaymentMock); + + $this->quotePaymentMock->expects($this->once()) + ->method('getAdditionalInformation') + ->with('shopper_conversion_id') + ->willReturn($this->mockShopperConversionId); + + // Mock Adyen Requests Helper call + $expectedRequestData = [ + 'amount' => [ + 'currency' => $mockCurrencyCode, + 'value' => $mockAmount, // Ensure the formatted value matches + ], + 'reference' => $mockReference, + 'shopperConversionId' => $this->mockShopperConversionId, + ]; + + + $orderMock = $this->createMock(Order::class); + $orderMock->method('getIncrementId')->willReturn($mockReference); + + + $paymentMock = $this->createMock(\Magento\Sales\Model\Order\Payment::class); + $paymentMock->method('getOrder')->willReturn($orderMock); + + $adyenAmountCurrencyMock = $this->createMock(AdyenAmountCurrency::class); + $adyenAmountCurrencyMock->method('getCurrencyCode')->willReturn($mockCurrencyCode); + $adyenAmountCurrencyMock->method('getAmount')->willReturn($mockAmount); + + $this->chargedCurrencyMock->method('getOrderAmountCurrency') + ->with($orderMock) + ->willReturn($adyenAmountCurrencyMock); + + $buildSubject = [ + 'payment' => $this->createConfiguredMock(PaymentDataObject::class, [ + 'getPayment' => $paymentMock + ]) + ]; + + $this->adyenRequestsHelperMock->expects($this->once()) + ->method('buildPaymentData') + ->with( + $mockAmount, + $mockCurrencyCode, + $mockReference, + $this->mockShopperConversionId, + [] + ) + ->willReturn($expectedRequestData); + + $result = $this->paymentDataBuilder->build($buildSubject); + $this->assertIsArray($result); + $this->assertArrayHasKey('body', $result); + $this->assertEquals($expectedRequestData, $result['body']); + } +} diff --git a/Test/Unit/Helper/GenerateShopperConversionIdTest.php b/Test/Unit/Helper/GenerateShopperConversionIdTest.php new file mode 100644 index 000000000..0b7a074f2 --- /dev/null +++ b/Test/Unit/Helper/GenerateShopperConversionIdTest.php @@ -0,0 +1,72 @@ +contextMock = $this->createMock(Context::class); + $this->checkoutSessionMock = $this->createMock(CheckoutSession::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->paymentMock = $this->createMock(Payment::class); + + $this->helper = new GenerateShopperConversionId( + $this->contextMock, + $this->checkoutSessionMock + ); + } + + /** + * Tear down the test environment + */ + protected function tearDown(): void + { + $this->helper = null; + parent::tearDown(); + } + + /** + * Test getShopperConversionId method + */ + public function testGetShopperConversionId(): void + { + $this->checkoutSessionMock->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteMock); + + $this->quoteMock->expects($this->once()) + ->method('getPayment') + ->willReturn($this->paymentMock); + + $this->quoteMock->expects($this->once()) + ->method('setPayment') + ->with($this->paymentMock); + + $this->quoteMock->expects($this->once()) + ->method('save'); + + $result = $this->helper->getShopperConversionId(); + + $this->assertNotNull($result); + } +} diff --git a/Test/Unit/Helper/PaymentMethodsTest.php b/Test/Unit/Helper/PaymentMethodsTest.php index f3260d0fa..0a05b5cae 100644 --- a/Test/Unit/Helper/PaymentMethodsTest.php +++ b/Test/Unit/Helper/PaymentMethodsTest.php @@ -51,6 +51,8 @@ use Adyen\AdyenException; use Exception; use ReflectionMethod; +use Adyen\Payment\Helper\GenerateShopperConversionId; +use Magento\Checkout\Model\Session as CheckoutSession; class PaymentMethodsTest extends AbstractAdyenTestCase { @@ -73,6 +75,8 @@ class PaymentMethodsTest extends AbstractAdyenTestCase private AdyenDataHelper $adyenDataHelperMock; private PaymentTokenRepositoryInterface $paymentTokenRepository; private SearchCriteriaBuilder $searchCriteriaBuilder; + private CheckoutSession $checkoutSession; + private GenerateShopperConversionId $generateShopperConversionId; protected function setUp(): void { @@ -116,6 +120,8 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); $this->objectManager = new ObjectManager($this); + $this->checkoutSession = $this->createMock(CheckoutSession::class); + $this->generateShopperConversionId = $this->createMock(GenerateShopperConversionId::class); // Instantiate the PaymentMethods helper class with the mocked dependencies $this->paymentMethodsHelper = new PaymentMethods( @@ -136,7 +142,9 @@ protected function setUp(): void $this->serializerMock, $this->adyenDataHelperMock, $this->paymentTokenRepository, - $this->searchCriteriaBuilder + $this->searchCriteriaBuilder, + $this->generateShopperConversionId, + $this->checkoutSession ); } @@ -419,59 +427,94 @@ public function testFetchPaymentMethodsWithEmptyResponseFromAdyenApi() $quoteId = 1; $storeId = 1; $amountValue = 100; + $shopperConversionId = '59e9d008-b427-47ae-b9b9-95c43d4ac3f6'; $adyenClientMock = $this->createMock(Client::class); $checkoutServiceMock = $this->createMock(Checkout\PaymentsApi::class); - // Setup test scenario + + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->onlyMethods(['getPayment', 'getStore', 'getBillingAddress']) + ->getMock(); + + // Mock store ID retrieval $this->storeMock->expects($this->any()) ->method('getId') ->willReturn($quoteId); - // Mock the getId method of the quote to return the quoteId + // Mock store retrieval from quote $this->quoteMock->expects($this->any()) ->method('getStore') ->willReturn($this->storeMock); + + // Mock fetching merchant account configuration $this->configHelperMock->expects($this->once()) ->method('getAdyenAbstractConfigData') - ->with('merchant_account', $storeId) // Ensure it's called with the expected parameters - ->willReturn('mocked_merchant_account'); // Define the return value for the mocked method + ->with('merchant_account', $storeId) + ->willReturn('mocked_merchant_account'); + + // Mock Adyen client and API initialization $this->adyenHelperMock->method('initializeAdyenClient')->willReturn($adyenClientMock); - $this->adyenHelperMock->method('initializePaymentsApi')->willReturn($checkoutServiceMock); + + // Mock currency details $this->amountCurrencyMock->method('getCurrencyCode')->willReturn('EUR'); $this->amountCurrencyMock->method('getAmount')->willReturn($amountValue); $this->chargedCurrencyMock->method('getQuoteAmountCurrency')->willReturn($this->amountCurrencyMock); + + // Mock billing address retrieval $this->billingAddressMock->expects($this->once()) ->method('getCountryId') ->willReturn('NL'); + $this->quoteMock ->method('getBillingAddress') ->willReturn($this->billingAddressMock); - // Simulate successful API call - $checkoutServiceMock->expects($this->once()) + + // Mock checkoutSession->getQuote() + $this->checkoutSession->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteMock); + + // Mock getPayment()->getAdditionalInformation('shopper_conversion_id') + $paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); + $paymentMock->expects($this->once()) + ->method('getAdditionalInformation') + ->with('shopper_conversion_id') + ->willReturn($shopperConversionId); + + // Mock getPayment() from quote + $this->quoteMock->expects($this->once()) + ->method('getPayment') + ->willReturn($paymentMock); + + // Simulate successful API call but return empty response + $checkoutServiceMock ->method('paymentMethods') ->willThrowException(new AdyenException("The Payment methods response is empty check your Adyen configuration in Magento.")); - $fetchPaymentMethodsMethod = $this->getPrivateMethod( PaymentMethods::class, 'fetchPaymentMethods' ); + // Create PaymentMethods object with mocked dependencies $paymentMethods = $this->objectManager->getObject( PaymentMethods::class, [ 'quote' => $this->quoteMock, 'configHelper' => $this->configHelperMock, 'chargedCurrency' => $this->chargedCurrencyMock, - 'adyenHelper' => $this->adyenHelperMock + 'adyenHelper' => $this->adyenHelperMock, + 'checkoutSession' => $this->checkoutSession ] ); - // Execute method of the tested class + // Execute the method being tested $result = $fetchPaymentMethodsMethod->invoke($paymentMethods, null, null); - // Assert conditions + // Assert that the response is an empty JSON array $this->assertEquals(json_encode([]), $result); } + public function testSuccessfulRetrievalOfPaymentMethods() { $expectedResult = [ @@ -490,6 +533,11 @@ public function testSuccessfulRetrievalOfPaymentMethods() $quoteId = 1; $storeId = 1; $amountValue = '100'; + $shopperConversionId = '59e9d008-b427-47ae-b9b9-95c43d4ac3f6'; + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->onlyMethods(['getPayment', 'getStore', 'getBillingAddress']) + ->getMock(); $requestParams = [ "channel" => "Web", @@ -500,7 +548,8 @@ public function testSuccessfulRetrievalOfPaymentMethods() "amount" => [ "currency" => 'EUR', "value" => $amountValue - ] + ], + 'shopperConversionId' => $shopperConversionId ]; $paymentMethodsExtraDetails['type']['configuration'] = [ @@ -558,6 +607,23 @@ public function testSuccessfulRetrievalOfPaymentMethods() $this->adyenHelperMock->expects($this->once()) ->method('logResponse'); + + // Mock checkoutSession->getQuote() + $this->checkoutSession->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteMock); + + $paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); + $paymentMock->expects($this->once()) + ->method('getAdditionalInformation') + ->with('shopper_conversion_id') + ->willReturn($shopperConversionId); + + // Mock getPayment() from quote + $this->quoteMock->expects($this->once()) + ->method('getPayment') + ->willReturn($paymentMock); + $paymentMethods = $this->objectManager->getObject( PaymentMethods::class, [ @@ -566,6 +632,7 @@ public function testSuccessfulRetrievalOfPaymentMethods() 'chargedCurrency' => $this->chargedCurrencyMock, 'adyenHelper' => $this->adyenHelperMock, 'paymentMethods' => $paymentMethodsMock, + 'checkoutSession' => $this->checkoutSession ] ); $fetchPaymentMethodsMethod = $this->getPrivateMethod( @@ -575,6 +642,7 @@ public function testSuccessfulRetrievalOfPaymentMethods() $result = $fetchPaymentMethodsMethod->invoke($paymentMethods, 'NL', 'nl_NL'); $this->assertJson($result); + } public function testGetCurrentCountryCodeWithBillingAddressSet() @@ -1091,6 +1159,7 @@ public function testGetPaymentMethodsRequest() $country = 'NL'; $amountValue = 100; $currencyCode = 'EUR'; + $shopperConversionId = '59e9d008-b427-47ae-b9b9-95c43d4ac3f6'; $this->amountCurrencyMock->method('getCurrencyCode')->willReturn('EUR'); $this->amountCurrencyMock->method('getAmount')->willReturn($amountValue); $this->chargedCurrencyMock->method('getQuoteAmountCurrency')->willReturn($this->amountCurrencyMock); @@ -1102,9 +1171,32 @@ public function testGetPaymentMethodsRequest() "merchantAccount" => $merchantAccount, "countryCode" => $country, "shopperLocale" => $shopperLocale, - "amount" => ["currency" => $currencyCode] + "amount" => ["currency" => $currencyCode], + "shopperConversionId" => "59e9d008-b427-47ae-b9b9-95c43d4ac3f6" ]; + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->onlyMethods(['getPayment', 'getStore', 'getBillingAddress']) + ->getMock(); + + // Mock checkoutSession->getQuote() + $this->checkoutSession->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteMock); + + // Mock getPayment()->getAdditionalInformation('shopper_conversion_id') + $paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class); + $paymentMock->expects($this->once()) + ->method('getAdditionalInformation') + ->with('shopper_conversion_id') + ->willReturn($shopperConversionId); + + // Mock getPayment() from quote + $this->quoteMock->expects($this->once()) + ->method('getPayment') + ->willReturn($paymentMock); + $paymentMethods = $this->objectManager->getObject( PaymentMethods::class, [ @@ -1112,6 +1204,7 @@ public function testGetPaymentMethodsRequest() 'configHelper' => $this->configHelperMock, 'chargedCurrency' => $this->chargedCurrencyMock, 'adyenHelper' => $this->adyenHelperMock, + 'checkoutSession' => $this->checkoutSession ] );