diff --git a/.github/Makefile b/.github/Makefile index 93f93bd5cc..01ed86b240 100644 --- a/.github/Makefile +++ b/.github/Makefile @@ -32,11 +32,6 @@ configure: n98-magerun2.phar bin/magento config:set payment/adyen_pay_by_link/active 1 bin/magento config:set payment/adyen_pay_by_link/days_to_expire 5 bin/magento config:set payment/adyen_giving/active 1 - bin/magento config:set payment/adyen_giving/charity_description 'test' - bin/magento config:set payment/adyen_giving/charity_website 'https://adyen.com' - bin/magento config:set payment/adyen_giving/charity_merchant_account "${DONATION_ACCOUNT}" - bin/magento config:set payment/adyen_giving/donation_amounts '1,5,10' - bin/magento config:set payment/adyen_giving/background_image '' bin/magento config:set payment/adyen_abstract/merchant_account "${ADYEN_MERCHANT}" bin/magento config:set payment/adyen_abstract/notifications_ip_check 0 bin/magento config:set payment/adyen_abstract/payment_authorized 'processing' diff --git a/Api/AdyenDonationCampaignsInterface.php b/Api/AdyenDonationCampaignsInterface.php new file mode 100644 index 0000000000..dca7eb860f --- /dev/null +++ b/Api/AdyenDonationCampaignsInterface.php @@ -0,0 +1,17 @@ +getOrder()->getPayment()->getAdditionalInformation('resultCode')) && @@ -100,7 +101,7 @@ public function getAction() return json_encode($this->getOrder()->getPayment()->getAdditionalInformation('action')); } - public function showAdyenGiving() + public function showAdyenGiving(): bool { return $this->adyenGivingEnabled() && $this->hasDonationToken(); } @@ -110,7 +111,7 @@ public function adyenGivingEnabled(): bool return (bool) $this->configHelper->adyenGivingEnabled($this->storeManager->getStore()->getId()); } - public function hasDonationToken() + public function hasDonationToken(): bool { return $this->getDonationToken() && 'null' !== $this->getDonationToken(); } @@ -120,25 +121,6 @@ public function getDonationToken() return json_encode($this->getOrder()->getPayment()->getAdditionalInformation('donationToken')); } - public function getDonationComponentConfiguration(): array - { - $storeId = $this->storeManager->getStore()->getId(); - $imageBaseUrl = $this->storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA).'adyen/'; - $donationAmounts = explode(',', (string) $this->configHelper->getAdyenGivingDonationAmounts($storeId)); - $donationAmounts = array_map(function ($amount) { - return $this->adyenHelper->formatAmount($amount, $this->getOrder()->getOrderCurrencyCode()); - }, $donationAmounts); - - return [ - 'name' => $this->configHelper->getAdyenGivingCharityName($storeId), - 'description' => $this->configHelper->getAdyenGivingCharityDescription($storeId), - 'backgroundUrl' => $imageBaseUrl . $this->configHelper->getAdyenGivingBackgroundImage($storeId), - 'logoUrl' => $imageBaseUrl . $this->configHelper->getAdyenGivingCharityLogo($storeId), - 'website' => $this->configHelper->getAdyenGivingCharityWebsite($storeId), - 'donationAmounts' => implode(',', $donationAmounts) - ]; - } - public function getSerializedCheckoutConfig() { return $this->serializerInterface->serialize($this->configProvider->getConfig()); @@ -157,7 +139,7 @@ public function getClientKey() return $this->configHelper->getClientKey($environment); } - public function getEnvironment() + public function getEnvironment(): string { return $this->adyenHelper->getCheckoutEnvironment( $this->storeManager->getStore()->getId() @@ -166,8 +148,9 @@ public function getEnvironment() /** * @return Order + * @throws LocalizedException */ - public function getOrder() + public function getOrder(): Order { if ($this->order == null) { $this->order = $this->orderRepository->get($this->checkoutSession->getLastOrderId()); @@ -175,6 +158,20 @@ public function getOrder() return $this->order; } + /** + * @return int + * @throws LocalizedException + */ + public function getOrderAmount() + { + if ($this->order == null) { + $this->order = $this->orderFactory->create()->load($this->checkoutSession->getLastOrderId()); + } + $amount = $this->order->getGrandTotal(); + $currency = $this->order->getOrderCurrencyCode(); + return $this->adyenHelper->formatAmount($amount, $currency); + } + /** * @throws NoSuchEntityException */ diff --git a/Helper/Config.php b/Helper/Config.php index 587dfb8ad0..cf166bd08e 100644 --- a/Helper/Config.php +++ b/Helper/Config.php @@ -351,53 +351,6 @@ public function adyenGivingEnabled($storeId) return $this->getConfigData('active', self::XML_ADYEN_GIVING_PREFIX, $storeId); } - public function getAdyenGivingConfigData($storeId) - { - return [ - 'name' => $this->getAdyenGivingCharityName($storeId), - 'description' => $this->getAdyenGivingCharityDescription($storeId), - 'backgroundUrl' => $this->getAdyenGivingBackgroundImage($storeId), - 'logoUrl' => $this->getAdyenGivingCharityLogo($storeId), - 'website' => $this->getAdyenGivingCharityWebsite($storeId), - 'donationAmounts' => $this->getAdyenGivingDonationAmounts($storeId) - ]; - } - - public function getAdyenGivingCharityName($storeId) - { - return $this->getConfigData('charity_name', self::XML_ADYEN_GIVING_PREFIX, $storeId); - } - - public function getAdyenGivingCharityDescription($storeId) - { - return $this->getConfigData('charity_description', self::XML_ADYEN_GIVING_PREFIX, $storeId); - } - - public function getAdyenGivingBackgroundImage($storeId) - { - return $this->getConfigData('background_image', self::XML_ADYEN_GIVING_PREFIX, $storeId); - } - - public function getAdyenGivingCharityLogo($storeId) - { - return $this->getConfigData('charity_logo', self::XML_ADYEN_GIVING_PREFIX, $storeId); - } - - public function getAdyenGivingCharityWebsite($storeId) - { - return $this->getConfigData('charity_website', self::XML_ADYEN_GIVING_PREFIX, $storeId); - } - - public function getAdyenGivingDonationAmounts($storeId) - { - return $this->getConfigData('donation_amounts', self::XML_ADYEN_GIVING_PREFIX, $storeId); - } - - public function getCharityMerchantAccount($storeId) - { - return $this->getConfigData('charity_merchant_account', self::XML_ADYEN_GIVING_PREFIX, $storeId); - } - /** * Retrieve payment_return_url config * diff --git a/Helper/Data.php b/Helper/Data.php index 3988d146ae..91f35b9214 100755 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -28,6 +28,7 @@ use Adyen\Service\Checkout\ModificationsApi; use Adyen\Service\Checkout\OrdersApi; use Adyen\Service\Checkout\PaymentLinksApi; +use Adyen\Service\Checkout\DonationsApi; use Adyen\Service\Checkout\PaymentsApi; use Adyen\Service\Checkout\UtilityApi; use Adyen\Service\PosPayment; @@ -1054,6 +1055,11 @@ public function initializePaymentLinksApi(Client $client):PaymentLinksApi return new PaymentLinksApi($client); } + public function initializeDonationsApi(Client $client):DonationsApi + { + return new DonationsApi($client); + } + /** * @param Client $client * @return PosPayment diff --git a/Helper/DonationsHelper.php b/Helper/DonationsHelper.php new file mode 100644 index 0000000000..ecff5dcce7 --- /dev/null +++ b/Helper/DonationsHelper.php @@ -0,0 +1,83 @@ +adyenHelper = $adyenHelper; + $this->adyenLogger = $adyenLogger; + } + + /** + * @throws NoSuchEntityException + * @throws LocalizedException + */ + public function fetchDonationCampaigns(array $payloadData, int $storeId): array + { + $request = new DonationCampaignsRequest($payloadData); + + try { + $client = $this->adyenHelper->initializeAdyenClient($storeId); + $service = $this->adyenHelper->initializeDonationsApi($client); + return $service->donationCampaigns($request)->toArray(); + } catch (\Adyen\AdyenException $e) { + $this->adyenLogger->error('Error fetching donation campaigns', ['exception' => $e]); + throw new LocalizedException(__('Unable to retrieve donation campaigns. Please try again later.')); + } + } + + //Return the data of the first campaign only. + public function formatCampaign(array $donationCampaignsResponse): array + { + $campaignList = $donationCampaignsResponse['donationCampaigns'] ?? []; + + if (empty($campaignList)) { + return []; + } + + $firstCampaign = $campaignList[0]; + + //Doing this workaround for now, Will be fixed with next API Library version + //RoundUp works with donation.type and not with donation.donationType + $firstCampaign['donation']['type'] = $firstCampaign['donation']['donationType'] ?? ''; + return [ + 'nonprofitName' => $firstCampaign['nonprofitName'] ?? '', + 'nonprofitDescription' => $firstCampaign['nonprofitDescription'] ?? '', + 'nonprofitUrl' => $firstCampaign['nonprofitUrl'] ?? '', + 'logoUrl' => $firstCampaign['logoUrl'] ?? '', + 'bannerUrl' => $firstCampaign['bannerUrl'] ?? '', + 'termsAndConditionsUrl' => $firstCampaign['termsAndConditionsUrl'] ?? '', + 'donation' => $firstCampaign['donation'] ?? [], + 'causeName' => $firstCampaign['causeName'] ?? '', + ]; + } + + public function setDonationCampaignId(Order $order, $campaignId): void + { + $order->getPayment()->setAdditionalInformation('donationCampaignId', $campaignId); + $order->save(); + } + +} diff --git a/Helper/Requests.php b/Helper/Requests.php index 0d6594d942..9b005e7301 100644 --- a/Helper/Requests.php +++ b/Helper/Requests.php @@ -30,7 +30,6 @@ class Requests extends AbstractHelper 'paywithgoogle' => 'scheme', 'applepay' => 'scheme' ]; - const SHOPPER_INTERACTION_CONTAUTH = 'ContAuth'; private Data $adyenHelper; private Config $adyenConfig; @@ -411,11 +410,10 @@ public function buildDonationData($buildSubject, $storeId): array 'type' => $paymentMethodCode ], 'donationToken' => $buildSubject['donationToken'], + 'donationCampaignId' => $buildSubject['donationCampaignId'], 'donationOriginalPspReference' => $buildSubject['donationOriginalPspReference'], - 'donationAccount' => $this->adyenConfig->getCharityMerchantAccount($storeId), 'returnUrl' => $buildSubject['returnUrl'], 'merchantAccount' => $this->adyenHelper->getAdyenMerchantAccount('adyen_giving', $storeId), - 'shopperInteraction' => self::SHOPPER_INTERACTION_CONTAUTH ]; } diff --git a/Model/Api/AdyenDonationCampaigns.php b/Model/Api/AdyenDonationCampaigns.php new file mode 100644 index 0000000000..c9104038bf --- /dev/null +++ b/Model/Api/AdyenDonationCampaigns.php @@ -0,0 +1,98 @@ +donationsHelper = $donationsHelper; + $this->orderRepository = $orderRepository; + $this->chargedCurrency = $chargedCurrency; + $this->adyenLogger = $adyenLogger; + $this->configHelper = $configHelper; + $this->adyenHelper = $adyenHelper; + } + + /** + * {@inheritdoc} + * @throws LocalizedException + */ + + public function getCampaigns(int $orderId): string + { + try { + $order = $this->orderRepository->get($orderId); + } catch (\Exception $e) { + $this->adyenLogger->error( + 'Cannot fetch donation campaigns.Failed to load order with ID ' . $orderId . ': ' . $e->getMessage() + ); + throw new LocalizedException(__('Unable to retrieve donation campaigns. Please try again later.')); + } + + if (!$order->getEntityId()) { + $this->adyenLogger->error("Order ID $orderId has no entity ID. Cannot fetch donation campaigns."); + throw new LocalizedException(__('Unable to retrieve donation campaigns. Please try again later.')); + } + + return $this->getCampaignData($order); + } + + /** + * @param OrderInterface $order + * @return string + * @throws LocalizedException + */ + public function getCampaignData(OrderInterface $order): string + { + $donationToken = $order->getPayment()->getAdditionalInformation('donationToken'); + if (!$donationToken) { + $this->adyenLogger->error('Missing donation token in payment additional information.'); + throw new LocalizedException(__('Unable to retrieve donation campaigns. Please try again later.')); + } + + $payloadData = []; + $orderAmountCurrency = $this->chargedCurrency->getOrderAmountCurrency($order, false); + $currencyCode = $orderAmountCurrency->getCurrencyCode(); + + //Creating payload + $payloadData['currency'] = $currencyCode; + $payloadData['merchantAccount'] = $this->configHelper->getMerchantAccount($order->getStoreId()); + $payloadData['locale'] = $this->adyenHelper->getCurrentLocaleCode($order->getStoreId()); + + try { + $donationCampaignsResponse = $this->donationsHelper->fetchDonationCampaigns($payloadData, $order->getStoreId()); + $campaignId = $donationCampaignsResponse['donationCampaigns'][0]['id']; + $this->donationsHelper->setDonationCampaignId($order, $campaignId); + $campaignsData = $this->donationsHelper->formatCampaign($donationCampaignsResponse); + return json_encode($campaignsData); + } catch (\Exception $e) { + $this->adyenLogger->error('Failed to fetch donation campaigns: ' . $e->getMessage()); + throw new LocalizedException(__('Unable to retrieve donation campaigns. Please try again later.')); + } + } + +} diff --git a/Model/Api/AdyenDonations.php b/Model/Api/AdyenDonations.php index 99e0cdb9e8..f72f1f53fc 100644 --- a/Model/Api/AdyenDonations.php +++ b/Model/Api/AdyenDonations.php @@ -82,9 +82,9 @@ public function makeDonation(string $payload, OrderInterface $order): void { $payload = $this->jsonSerializer->unserialize($payload); - $payment = $order->getPayment(); - $paymentMethodInstance = $payment->getMethodInstance(); - $donationToken = $payment->getAdditionalInformation('donationToken'); + $paymentMethodInstance = $order->getPayment()->getMethodInstance(); + $donationToken = $order->getPayment()->getAdditionalInformation('donationToken'); + $donationCampaignId = $order->getPayment()->getAdditionalInformation('donationCampaignId'); if (!$donationToken) { throw new LocalizedException(__('Donation failed!')); @@ -95,19 +95,8 @@ public function makeDonation(string $payload, OrderInterface $order): void throw new LocalizedException(__('Donation failed!')); } - $donationAmounts = explode(',', $this->config->getAdyenGivingDonationAmounts($order->getStoreId())); - $formatter = $this->dataHelper; - $donationAmountsMinorUnits = array_map( - function ($amount) use ($formatter, $currencyCode) { - return $formatter->formatAmount($amount, $currencyCode); - }, - $donationAmounts - ); - if (!in_array($payload['amount']['value'], $donationAmountsMinorUnits)) { - throw new LocalizedException(__('Donation failed!')); - } - $payload['donationToken'] = $donationToken; + $payload['donationCampaignId'] = $donationCampaignId; $payload['donationOriginalPspReference'] = $payment->getAdditionalInformation('pspReference'); // Override payment method object with payment method code @@ -133,15 +122,17 @@ function ($amount) use ($formatter, $currencyCode) { $donationsCaptureCommand = $this->commandPool->get('capture'); $donationsCaptureCommand->execute(['payment' => $payload]); - // Remove donation token after a successfull donation. + // Remove donation token & DonationCampaignId after a successful donation. $this->removeDonationToken($order); + $this->removeDonationCampaignId($order); } catch (LocalizedException $e) { $this->donationTryCount = $payment->getAdditionalInformation('donationTryCount'); if ($this->donationTryCount >= 5) { - // Remove donation token after 5 try and throw a exception. + // Remove donation token and DonationCampaignId after 5 try and throw a exception. $this->removeDonationToken($order); + $this->removeDonationCampaignId($order); } $this->incrementTryCount($order); @@ -170,4 +161,10 @@ private function removeDonationToken(Order $order): void $payment->unsAdditionalInformation('donationToken'); $this->orderRepository->save($order); } + + private function removeDonationCampaignId(Order $order): void + { + $order->getPayment()->unsAdditionalInformation('donationCampaignId'); + $order->save(); + } } diff --git a/Model/Api/GuestAdyenDonationCampaigns.php b/Model/Api/GuestAdyenDonationCampaigns.php new file mode 100644 index 0000000000..37bdb35807 --- /dev/null +++ b/Model/Api/GuestAdyenDonationCampaigns.php @@ -0,0 +1,44 @@ +quoteIdMaskFactory = $quoteIdMaskFactory; + $this->orderRepository = $orderRepository; + $this->adyenDonationCampaigns = $adyenDonationCampaigns; + } + + /** + * {@inheritdoc} + */ + public function getCampaigns(string $cartId): string + { + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + $quoteId = $quoteIdMask->getQuoteId(); + + if (!$quoteId) { + throw new LocalizedException(__('Invalid cart ID.')); + } + + $order = $this->orderRepository->getOrderByQuoteId($quoteId); + + return $this->adyenDonationCampaigns->getCampaignData($order); + } +} diff --git a/Model/Ui/AdyenCheckoutSuccessConfigProvider.php b/Model/Ui/AdyenCheckoutSuccessConfigProvider.php index ba60a613ec..b5bdc3ae2b 100755 --- a/Model/Ui/AdyenCheckoutSuccessConfigProvider.php +++ b/Model/Ui/AdyenCheckoutSuccessConfigProvider.php @@ -12,6 +12,7 @@ namespace Adyen\Payment\Model\Ui; use Magento\Checkout\Model\ConfigProviderInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; class AdyenCheckoutSuccessConfigProvider implements ConfigProviderInterface @@ -29,6 +30,7 @@ public function __construct( /** * @return array + * @throws NoSuchEntityException */ public function getConfig() { diff --git a/Test/Unit/AbstractAdyenTestCase.php b/Test/Unit/AbstractAdyenTestCase.php index d9f222e77c..9517955c4a 100644 --- a/Test/Unit/AbstractAdyenTestCase.php +++ b/Test/Unit/AbstractAdyenTestCase.php @@ -134,4 +134,4 @@ protected function invokeMethod(&$object, $methodName, array $parameters = []) return $method->invokeArgs($object, $parameters); } -} +} \ No newline at end of file diff --git a/Test/Unit/Block/Checkout/SuccessTest.php b/Test/Unit/Block/Checkout/SuccessTest.php index d5b2784668..bffa37f32b 100644 --- a/Test/Unit/Block/Checkout/SuccessTest.php +++ b/Test/Unit/Block/Checkout/SuccessTest.php @@ -12,6 +12,7 @@ namespace Adyen\Payment\Test\Unit\Block\Checkout; use Adyen\Payment\Block\Checkout\Success; +use Adyen\Payment\Helper\PaymentResponseHandler; use Adyen\Payment\Helper\Config; use Adyen\Payment\Helper\Data; use Adyen\Payment\Model\Ui\AdyenCheckoutSuccessConfigProvider; @@ -24,6 +25,12 @@ use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Model\Session as CustomerSession; use Magento\Quote\Model\QuoteIdToMaskedQuoteId; +use Adyen\Payment\Helper\Config; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use Adyen\Payment\Model\Ui\AdyenCheckoutSuccessConfigProvider; +use Magento\Framework\Serialize\SerializerInterface; +use Adyen\Payment\Helper\Data; use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; @@ -45,6 +52,8 @@ class SuccessTest extends AbstractAdyenTestCase protected function setUp(): void { + $storeId = 1; + $this->objectManager = new ObjectManager($this); $this->checkoutSessionMock = $this->createGeneratedMock( CheckoutSession::class, [], @@ -54,9 +63,34 @@ protected function setUp(): void CustomerSession::class, ['isLoggedIn'] ); + $this->paymentMock = $this->createMock(Order\Payment::class); $this->orderFactoryMock = $this->createGeneratedMock(OrderFactory::class, ['create']); $this->orderMock = $this->createMock(Order::class); $this->quoteIdToMaskedQuoteIdMock = $this->createMock(QuoteIdToMaskedQuoteId::class); + $this->configProviderMock = $this->createMock(AdyenCheckoutSuccessConfigProvider::class); + $this->serializerMock = $this->createMock(SerializerInterface::class); + $this->configHelperMock = $this->createMock(Config::class); + $this->adyenHelperMock = $this->createMock(Data::class); + $storeMock = $this->createConfiguredMock(StoreInterface::class, [ + 'getId' => $storeId + ]); + $this->storeManagerMock = $this->createConfiguredMock(StoreManagerInterface::class, [ + 'getStore' => $storeMock + ]); + + $this->successBlock = $this->objectManager->getObject( + Success::class, + [ + 'checkoutSession' => $this->checkoutSessionMock, + 'customerSession' => $this->customerSessionMock, + 'orderFactory' => $this->orderFactoryMock, + 'quoteIdToMaskedQuoteId' => $this->quoteIdToMaskedQuoteIdMock, + 'configHelper' => $this->configHelperMock, + 'storeManager' => $this->storeManagerMock, + 'serializerInterface' => $this->serializerMock, + 'configProvider' => $this->configProviderMock, + 'adyenHelper' => $this->adyenHelperMock + ] $this->adyenDataHelper = $this->createMock(Data::class); $this->contextMock = $this->createMock(Context::class); $this->orderRepositoryMock = $this->createMock(OrderRepositoryInterface::class); @@ -116,4 +150,170 @@ public function testGetIsCustomerLoggedIn() $this->assertTrue($this->successBlock->getIsCustomerLoggedIn()); } + + public function testRenderActionReturnsTrue() + { + $this->paymentMock->method('getAdditionalInformation')->willReturnCallback(function ($key) { + return match ($key) { + 'resultCode' => PaymentResponseHandler::RECEIVED, + 'action' => ['type' => 'voucher'] + }; + }); + + $this->orderMock->method('load')->willReturnSelf(); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->orderFactoryMock->method('create')->willReturn($this->orderMock); + $this->checkoutSessionMock->method('getLastOrderId')->willReturn(123); + + $this->assertTrue($this->successBlock->renderAction()); + } + + public function testRenderActionReturnsFalse() + { + $this->paymentMock->method('getAdditionalInformation')->willReturnCallback(function ($key) { + return match ($key) { + 'resultCode' => PaymentResponseHandler::AUTHORISED, + 'action' => '' + }; + }); + + $this->orderMock->method('load')->willReturnSelf(); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->orderFactoryMock->method('create')->willReturn($this->orderMock); + $this->checkoutSessionMock->method('getLastOrderId')->willReturn(123); + + $this->assertFalse($this->successBlock->renderAction()); + } + + public function testGetAction() + { + $expectedAction = ['type' => 'voucher']; + + $this->paymentMock->method('getAdditionalInformation')->willReturnCallback(function ($key) { + return match ($key) { + 'action' => ['type' => 'voucher'] + }; + }); + + $this->orderMock->method('load')->willReturnSelf(); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->orderFactoryMock->method('create')->willReturn($this->orderMock); + $this->checkoutSessionMock->method('getLastOrderId')->willReturn(123); + + $this->assertEquals(json_encode($expectedAction), $this->successBlock->getAction()); + } + + public function testGetDonationToken() + { + $expectedToken = 'sample_donation_token'; + + $this->paymentMock->method('getAdditionalInformation')->willReturnCallback(function ($key) { + return match ($key) { + 'donationToken' => 'sample_donation_token' + }; + }); + + $this->orderMock->method('load')->willReturnSelf(); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->orderFactoryMock->method('create')->willReturn($this->orderMock); + $this->checkoutSessionMock->method('getLastOrderId')->willReturn(123); + + $this->assertEquals(json_encode($expectedToken), $this->successBlock->getDonationToken()); + } + + public function testAdyenGivingEnabled() + { + $storeId = 1; + + $this->configHelperMock->method('adyenGivingEnabled')->with($storeId)->willReturn(true); + $this->assertTrue($this->successBlock->adyenGivingEnabled()); + } + + public function testGetMerchantAccount() + { + $storeId = 1; + $merchantAccount = 'TestMerchant'; + + $this->configHelperMock->method('getMerchantAccount')->with($storeId)->willReturn($merchantAccount); + + $this->assertEquals($merchantAccount, $this->successBlock->getMerchantAccount()); + } + + public function testGetSerializedCheckoutConfig() + { + $configData = ['some' => 'config']; + $serialized = '{"some":"config"}'; + + $this->configProviderMock->method('getConfig')->willReturn($configData); + $this->serializerMock->method('serialize')->with($configData)->willReturn($serialized); + + $this->assertEquals($serialized, $this->successBlock->getSerializedCheckoutConfig()); + } + + public function testGetEnvironment() + { + $storeId = 1; + $environment = 'test'; + + $this->adyenHelperMock->method('getCheckoutEnvironment')->with($storeId)->willReturn($environment); + + $this->successBlock = $this->objectManager->getObject( + Success::class, + [ + 'checkoutSession' => $this->checkoutSessionMock, + 'customerSession' => $this->customerSessionMock, + 'orderFactory' => $this->orderFactoryMock, + 'quoteIdToMaskedQuoteId' => $this->quoteIdToMaskedQuoteIdMock, + 'adyenHelper' => $this->adyenHelperMock, + 'storeManager' => $this->storeManagerMock + ] + ); + + $this->assertEquals($environment, $this->successBlock->getEnvironment()); + } + + public function testGetLocale() + { + $storeId = 1; + $locale = 'en_US'; + + + $this->adyenHelperMock->method('getCurrentLocaleCode')->with($storeId)->willReturn($locale); + + $this->successBlock = $this->objectManager->getObject( + Success::class, + [ + 'checkoutSession' => $this->checkoutSessionMock, + 'customerSession' => $this->customerSessionMock, + 'orderFactory' => $this->orderFactoryMock, + 'quoteIdToMaskedQuoteId' => $this->quoteIdToMaskedQuoteIdMock, + 'adyenHelper' => $this->adyenHelperMock, + 'storeManager' => $this->storeManagerMock + ] + ); + + $this->assertEquals($locale, $this->successBlock->getLocale()); + } + + public function testGetClientKey() + { + $clientKey = 'test_client_key'; + + $this->configHelperMock->method('isDemoMode')->willReturn(true); + $this->configHelperMock->method('getClientKey')->with('test')->willReturn($clientKey); + + $this->successBlock = $this->objectManager->getObject( + Success::class, + [ + 'checkoutSession' => $this->checkoutSessionMock, + 'customerSession' => $this->customerSessionMock, + 'orderFactory' => $this->orderFactoryMock, + 'quoteIdToMaskedQuoteId' => $this->quoteIdToMaskedQuoteIdMock, + 'configHelper' => $this->configHelperMock + ] + ); + + $this->assertEquals($clientKey, $this->successBlock->getClientKey()); + } + } diff --git a/Test/Unit/Helper/DataTest.php b/Test/Unit/Helper/DataTest.php index 0c8ab2f2b5..ad61d2495b 100755 --- a/Test/Unit/Helper/DataTest.php +++ b/Test/Unit/Helper/DataTest.php @@ -25,6 +25,7 @@ use Adyen\Payment\Model\ResourceModel\Notification\CollectionFactory as NotificationCollectionFactory; use Adyen\Payment\Observer\AdyenPaymentMethodDataAssignObserver; use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Adyen\Service\Checkout\DonationsApi; use Adyen\Service\Checkout\ModificationsApi; use Adyen\Service\Checkout\OrdersApi; use Adyen\Service\Checkout\PaymentLinksApi; @@ -1758,6 +1759,12 @@ public function testInitializePaymentLinksApi() $this->assertInstanceOf(PaymentLinksApi::class, $service); } + public function testInitializeDonationsApi() + { + $service = $this->dataHelper->initializeDonationsApi($this->clientMock); + $this->assertInstanceOf(DonationsApi::class, $service); + } + public function testLogAdyenException() { $this->store->method('getId')->willReturn(1); diff --git a/Test/Unit/Helper/DonationsHelperTest.php b/Test/Unit/Helper/DonationsHelperTest.php new file mode 100644 index 0000000000..74a7f32d2a --- /dev/null +++ b/Test/Unit/Helper/DonationsHelperTest.php @@ -0,0 +1,143 @@ +createMock(Context::class); + $this->adyenHelperMock = $this->createMock(Data::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + + $this->donationsHelper = new DonationsHelper( + $contextMock, + $this->adyenHelperMock, + $this->adyenLoggerMock + ); + } + + public function testFetchDonationCampaignsSuccess(): void + { + $storeId = 1; + $payload = [ + 'merchantAccount' => 'TestMerchant', + 'currency' => 'EUR', + 'locale' => 'en-US' + ]; + + $clientMock = $this->createMock(Client::class); + $donationsApiMock = $this->createMock(Checkout\DonationsApi::class); + + $responseMock = $this->createMock(DonationCampaignsResponse::class); + + $expected = ['donationCampaigns' => [['id' => 'abc']]]; + + $this->adyenHelperMock->method('initializeAdyenClient')->with($storeId)->willReturn($clientMock); + $this->adyenHelperMock->method('initializeDonationsApi')->with($clientMock)->willReturn($donationsApiMock); + $donationsApiMock->method('donationCampaigns')->with($this->isInstanceOf(DonationCampaignsRequest::class)) + ->willReturn($responseMock); + $responseMock->method('toArray')->willReturn($expected); + + $result = $this->donationsHelper->fetchDonationCampaigns($payload, $storeId); + $this->assertEquals($expected, $result); + } + + public function testFetchDonationCampaignsThrowsAndLogs(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Unable to retrieve donation campaigns'); + + $storeId = 1; + $payload = [ + 'merchantAccount' => 'TestMerchant', + 'currency' => 'EUR', + 'locale' => 'en-US' + ]; + + $clientMock = $this->createMock(\Adyen\Client::class); + + $this->adyenHelperMock->method('initializeAdyenClient')->with($storeId)->willReturn($clientMock); + $this->adyenHelperMock->method('initializeDonationsApi') + ->with($clientMock) + ->willThrowException(new AdyenException('API failed')); + + $this->adyenLoggerMock->expects($this->once()) + ->method('error') + ->with( + $this->stringContains('Error fetching donation campaigns'), + $this->arrayHasKey('exception') + ); + + $this->donationsHelper->fetchDonationCampaigns($payload, $storeId); + } + + public function testFormatCampaignWithData(): void + { + $response = [ + 'donationCampaigns' => [[ + 'nonprofitName' => 'Red Cross', + 'nonprofitDescription' => 'Helping people', + 'nonprofitUrl' => 'https://example.com', + 'logoUrl' => 'https://example.com/logo.png', + 'bannerUrl' => 'https://example.com/banner.png', + 'termsAndConditionsUrl' => 'https://example.com/terms', + 'donation' => ['amount' => 500, 'type' => 'roundup'], + 'causeName' => 'Adyen Giving' + ]] + ]; + + $expected = [ + 'nonprofitName' => 'Red Cross', + 'nonprofitDescription' => 'Helping people', + 'nonprofitUrl' => 'https://example.com', + 'logoUrl' => 'https://example.com/logo.png', + 'bannerUrl' => 'https://example.com/banner.png', + 'termsAndConditionsUrl' => 'https://example.com/terms', + 'donation' => ['amount' => 500, 'type' => ''], + 'causeName' => 'Adyen Giving' + ]; + + $result = $this->donationsHelper->formatCampaign($response); + $this->assertEquals($expected, $result); + } + + public function testFormatCampaignWithEmptyData(): void + { + $this->assertEquals([], $this->donationsHelper->formatCampaign([])); + $this->assertEquals([], $this->donationsHelper->formatCampaign(['donationCampaigns' => []])); + } + + public function testSetDonationCampaignId(): void + { + $orderMock = $this->createMock(Order::class); + $paymentMock = $this->createMock(Payment::class); + + $orderMock->method('getPayment')->willReturn($paymentMock); + $paymentMock->expects($this->once()) + ->method('setAdditionalInformation') + ->with('donationCampaignId', 'CAMPAIGN_ID'); + + $orderMock->expects($this->once())->method('save'); + + $this->donationsHelper->setDonationCampaignId($orderMock, 'CAMPAIGN_ID'); + } +} diff --git a/Test/Unit/Helper/RequestsTest.php b/Test/Unit/Helper/RequestsTest.php index 8a893460fa..35bd1c8d29 100644 --- a/Test/Unit/Helper/RequestsTest.php +++ b/Test/Unit/Helper/RequestsTest.php @@ -382,17 +382,12 @@ public function testBuildDonationDataWithValidData(): void 'shopperReference' => 'shopper123', 'donationToken' => 'donationToken123', 'donationOriginalPspReference' => 'originalPspReference123', - 'returnUrl' => 'https://example.com/return' + 'returnUrl' => 'https://example.com/return', + 'id' => 12 ]; $storeId = 1; - // Mock the charity merchant account return - $this->adyenConfigMock - ->method('getCharityMerchantAccount') - ->with($storeId) - ->willReturn('charityMerchantAccount123'); - // Mock the merchant account return $this->adyenHelperMock ->expects($this->once()) @@ -410,7 +405,7 @@ public function testBuildDonationDataWithValidData(): void $this->assertArrayHasKey('paymentMethod', $result); $this->assertArrayHasKey('donationToken', $result); $this->assertArrayHasKey('donationOriginalPspReference', $result); - $this->assertArrayHasKey('donationAccount', $result); + $this->assertArrayHasKey('donationCampaignId', $result); $this->assertArrayHasKey('returnUrl', $result); $this->assertArrayHasKey('merchantAccount', $result); $this->assertArrayHasKey('shopperInteraction', $result); @@ -432,7 +427,8 @@ public function testBuildDonationDataWithMappedPaymentMethod(): void 'shopperReference' => 'shopper456', 'donationToken' => 'donationToken456', 'donationOriginalPspReference' => 'originalPspReference456', - 'returnUrl' => 'https://example.com/return' + 'returnUrl' => 'https://example.com/return', + 'id' => 12 ]; $storeId = 2; diff --git a/Test/Unit/Model/Api/AdyenDonationCampaignsTest.php b/Test/Unit/Model/Api/AdyenDonationCampaignsTest.php new file mode 100644 index 0000000000..44d6958b32 --- /dev/null +++ b/Test/Unit/Model/Api/AdyenDonationCampaignsTest.php @@ -0,0 +1,163 @@ +donationsHelperMock = $this->createMock(DonationsHelper::class); + $this->orderRepositoryMock = $this->createMock(OrderRepository::class); + $this->chargedCurrencyMock = $this->createMock(ChargedCurrency::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + $this->configHelperMock = $this->createMock(Config::class); + $this->adyenHelperMock = $this->createMock(AdyenHelper::class); + $this->currencyObject = $this->createMock(AdyenAmountCurrency::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + + $this->adyenDonationCampaigns = new AdyenDonationCampaigns( + $this->donationsHelperMock, + $this->orderRepositoryMock, + $this->chargedCurrencyMock, + $this->adyenLoggerMock, + $this->configHelperMock, + $this->adyenHelperMock + ); + } + + public function testGetCampaignsSuccess(): void + { + $orderId = 100; + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->method('getEntityId')->willReturn($orderId); + + $this->orderRepositoryMock->method('get')->willReturn($orderMock); + + $this->adyenDonationCampaigns = $this->getMockBuilder(AdyenDonationCampaigns::class) + ->setConstructorArgs([ + $this->donationsHelperMock, + $this->orderRepositoryMock, + $this->chargedCurrencyMock, + $this->adyenLoggerMock, + $this->configHelperMock, + $this->adyenHelperMock + ]) + ->onlyMethods(['getCampaignData']) + ->getMock(); + + $this->adyenDonationCampaigns->expects($this->once()) + ->method('getCampaignData') + ->with($orderMock) + ->willReturn(json_encode(['key' => 'value'])); + + $result = $this->adyenDonationCampaigns->getCampaigns($orderId); + $this->assertEquals(json_encode(['key' => 'value']), $result); + } + + public function testGetCampaignsThrowsIfOrderLoadFails(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Unable to retrieve donation campaigns'); + + $orderId = 999; + $this->orderRepositoryMock->method('get')->willThrowException(new \Exception('DB fail')); + + $this->adyenLoggerMock->expects($this->once()) + ->method('error') + ->with($this->stringContains("Failed to load order with ID")); + + $this->adyenDonationCampaigns->getCampaigns($orderId); + } + + public function testGetCampaignsThrowsIfNoEntityId(): void + { + $this->expectException(LocalizedException::class); + + $orderId = 101; + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->method('getEntityId')->willReturn(null); + + $this->orderRepositoryMock->method('get')->willReturn($orderMock); + + $this->adyenLoggerMock->expects($this->once()) + ->method('error') + ->with($this->stringContains("Order ID $orderId has no entity ID")); + + $this->adyenDonationCampaigns->getCampaigns($orderId); + } + + public function testGetCampaignDataSuccess(): void + { + $currencyCode = 'EUR'; + $merchantAccount = 'TestMerchant'; + $locale = 'en_US'; + $campaignId = 'abc123'; + + $this->paymentMock->method('getAdditionalInformation')->with('donationToken')->willReturn('token123'); + $this->orderMock->method('getPayment')->willReturn($this->paymentMock); + $this->orderMock->method('getStoreId')->willReturn(1); + + $this->currencyObject->method('getCurrencyCode')->willReturn($currencyCode); + + $this->chargedCurrencyMock->method('getOrderAmountCurrency') + ->with($this->orderMock, false) + ->willReturn($this->currencyObject); + + $this->configHelperMock->method('getMerchantAccount')->willReturn($merchantAccount); + $this->adyenHelperMock->method('getCurrentLocaleCode')->willReturn($locale); + + $donationCampaignsResponse = ['donationCampaigns' => [['id' => $campaignId]]]; + $formattedCampaign = ['reference' => 'abc']; + + $this->donationsHelperMock->method('fetchDonationCampaigns')->willReturn($donationCampaignsResponse); + $this->donationsHelperMock->method('formatCampaign')->willReturn($formattedCampaign); + $this->donationsHelperMock->expects($this->once()) + ->method('setDonationCampaignId') + ->with($this->orderMock, $campaignId); + + $result = $this->adyenDonationCampaigns->getCampaignData($this->orderMock); + $this->assertEquals(json_encode($formattedCampaign), $result); + } + + public function testGetCampaignDataThrowsIfNoDonationToken(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Unable to retrieve donation campaigns'); + + $paymentMock = $this->createMock(Payment::class); + $paymentMock->method('getAdditionalInformation')->with('donationToken')->willReturn(null); + + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->method('getPayment')->willReturn($paymentMock); + + $this->adyenLoggerMock->expects($this->once()) + ->method('error') + ->with($this->stringContains('Missing donation token')); + + $this->adyenDonationCampaigns->getCampaignData($orderMock); + } +} diff --git a/Test/Unit/Model/Api/AdyenDonationsTest.php b/Test/Unit/Model/Api/AdyenDonationsTest.php index c731bd3a96..66f5ebed32 100644 --- a/Test/Unit/Model/Api/AdyenDonationsTest.php +++ b/Test/Unit/Model/Api/AdyenDonationsTest.php @@ -191,6 +191,22 @@ public function testDonate($paymentMethod, $paymentMethodGroup, $customerId, $ex $this->commandPoolMock->expects($this->once()) ->method('get') + ->willReturn($this->createMock(OrderInterface::class)); + + $adyenDonationsMock = $this->getMockBuilder(AdyenDonations::class) + ->onlyMethods(['makeDonation']) + ->setConstructorArgs([ + $this->createMock(CommandPoolInterface::class), + $this->createMock(Json::class), + $this->createMock(Data::class), + $this->createMock(ChargedCurrency::class), + $this->createMock(Config::class), + $this->createMock(PaymentMethods::class), + $orderRepositoryMock + ]) + ->getMock(); + + $adyenDonationsMock->donate(1, ''); ->with('capture') ->willReturn($donationCommand); diff --git a/Test/Unit/Model/Api/GuestAdyenDonationCampaignsTest.php b/Test/Unit/Model/Api/GuestAdyenDonationCampaignsTest.php new file mode 100644 index 0000000000..50577670d5 --- /dev/null +++ b/Test/Unit/Model/Api/GuestAdyenDonationCampaignsTest.php @@ -0,0 +1,68 @@ +quoteIdMaskFactoryMock = $this->createMock(QuoteIdMaskFactory::class); + $this->adyenDonationCampaignsMock = $this->createMock(AdyenDonationCampaigns::class); + $this->orderRepositoryMock = $this->createMock(OrderRepository::class); + + $this->guestDonationCampaigns = new GuestAdyenDonationCampaigns( + $this->quoteIdMaskFactoryMock, + $this->orderRepositoryMock, + $this->adyenDonationCampaignsMock + ); + } + + public function testGetCampaignsSuccess(): void + { + $cartId = '123'; + $quoteId = 42; + + // Mock QuoteIdMask and its factory + $quoteIdMaskMock = $this->createGeneratedMock( + QuoteIdMask::class, + ['load'], + ['getQuoteId'] + ); + + $quoteIdMaskMock->method('load')->willReturn($quoteIdMaskMock); + $quoteIdMaskMock->method('getQuoteId')->willReturn($quoteId); + + $this->quoteIdMaskFactoryMock->method('create')->willReturn($quoteIdMaskMock); + + // Mock Order object + $orderMock = $this->createMock(Order::class); + $this->orderRepositoryMock->method('getOrderByQuoteId')->with($quoteId)->willReturn($orderMock); + + $expectedResponse = json_encode(['donationCampaigns' => [['reference' => 'abc']]]); + + // Mock call to getCampaignData + $this->adyenDonationCampaignsMock->expects($this->once()) + ->method('getCampaignData') + ->with($orderMock) + ->willReturn($expectedResponse); + + $result = $this->guestDonationCampaigns->getCampaigns($cartId); + + $this->assertEquals($expectedResponse, $result); + } + +} diff --git a/etc/adminhtml/system/adyen_giving.xml b/etc/adminhtml/system/adyen_giving.xml index 43cd64f7fd..3aa2dec435 100644 --- a/etc/adminhtml/system/adyen_giving.xml +++ b/etc/adminhtml/system/adyen_giving.xml @@ -22,47 +22,5 @@ payment/adyen_giving/active - - - payment/adyen_giving/charity_name - - - - - payment/adyen_giving/charity_description - - - - - payment/adyen_giving/charity_website - - - - - payment/adyen_giving/charity_merchant_account - - - - - - Adyen\Payment\Model\Config\Backend\DonationAmounts - payment/adyen_giving/donation_amounts - - - - - Magento\Config\Model\Config\Backend\Image - payment/adyen_giving/background_image - adyen - adyen - - diff --git a/etc/config.xml b/etc/config.xml index 62bb8ba5cf..c9a725d3a0 100755 --- a/etc/config.xml +++ b/etc/config.xml @@ -2607,6 +2607,9 @@ 1 adyen-alternative-payment-method + + 1 + - \ No newline at end of file + diff --git a/etc/di.xml b/etc/di.xml index 74c1af8e44..eff70fdded 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1640,6 +1640,8 @@ + + diff --git a/etc/webapi.xml b/etc/webapi.xml index 57913553c6..39a18277c2 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -109,7 +109,7 @@ - + @@ -124,6 +124,20 @@ + + + + + + + + + + + + + + diff --git a/view/frontend/templates/checkout/success.phtml b/view/frontend/templates/checkout/success.phtml index 08b029c531..af580af555 100644 --- a/view/frontend/templates/checkout/success.phtml +++ b/view/frontend/templates/checkout/success.phtml @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2015 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2022 Adyen N.V. (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -12,104 +12,153 @@ /** * @var \Adyen\Payment\Block\Checkout\Success $block */ -?> - +script; +?> + +renderTag('script', [], $scriptCartReload, false) ?> + renderAction()): ?> - -
+script; + ?> + + renderTag('script', [], $scriptAction, false) ?> -showAdyenGiving()): - $checkoutConfig = /* @noEscape */ - $block->getSerializedCheckoutConfig(); - $scriptString = __('window.checkoutConfig = %1;', $checkoutConfig); -?> - -
+script; + ?> + + renderTag('script', [], $scriptGiving, false) ?> diff --git a/view/frontend/web/js/model/adyen-payment-service.js b/view/frontend/web/js/model/adyen-payment-service.js index 73ec180deb..b129d7e5b8 100644 --- a/view/frontend/web/js/model/adyen-payment-service.js +++ b/view/frontend/web/js/model/adyen-payment-service.js @@ -131,6 +131,25 @@ define( cartId: maskedQuoteId }); } + return storage.post( + serviceUrl, + JSON.stringify(request), + true + ); + }, + + donationCampaigns: function (isLoggedIn, orderId, maskedQuoteId) { + let serviceUrl; + let request = {}; + + if (isLoggedIn) { + serviceUrl = urlBuilder.createUrl('/adyen/orders/carts/mine/donation-campaigns', {}); + request.orderId = orderId; + } else { + serviceUrl = urlBuilder.createUrl('/adyen/orders/guest-carts/:cartId/donation-campaigns', { + cartId: maskedQuoteId + }); + } return storage.post( serviceUrl,