From 480342bd35ff5dd02760bafc5295954eff141e40 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 16 Feb 2026 15:59:53 +0100 Subject: [PATCH 1/4] [ECP-8667] Indicate the donations' availability in the payment status --- Helper/PaymentResponseHandler.php | 8 +++++++- Model/Api/AdyenOrderPaymentStatus.php | 3 ++- Model/Api/AdyenPaymentsDetails.php | 2 +- Model/Api/GuestAdyenOrderPaymentStatus.php | 2 +- etc/schema.graphqls | 1 + 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Helper/PaymentResponseHandler.php b/Helper/PaymentResponseHandler.php index 5ef5053f19..0f17322086 100644 --- a/Helper/PaymentResponseHandler.php +++ b/Helper/PaymentResponseHandler.php @@ -74,10 +74,16 @@ public function __construct( public function formatPaymentResponse( string $resultCode, - ?array $action = null + ?array $action = null, + ?bool $donationTokenExists = false ): array { switch ($resultCode) { case self::AUTHORISED: + return [ + "isFinal" => true, + "resultCode" => $resultCode, + "canDonate" => $donationTokenExists + ]; case self::REFUSED: case self::ERROR: case self::POS_SUCCESS: diff --git a/Model/Api/AdyenOrderPaymentStatus.php b/Model/Api/AdyenOrderPaymentStatus.php index 55cc124b4a..e12d57010d 100644 --- a/Model/Api/AdyenOrderPaymentStatus.php +++ b/Model/Api/AdyenOrderPaymentStatus.php @@ -61,7 +61,8 @@ public function getOrderPaymentStatus(string $orderId): string return json_encode($this->paymentResponseHandler->formatPaymentResponse( $additionalInformation['resultCode'], - !empty($additionalInformation['action']) ? $additionalInformation['action'] : null + !empty($additionalInformation['action']) ? $additionalInformation['action'] : null, + !empty($additionalInformation['donationToken']) )); } } diff --git a/Model/Api/AdyenPaymentsDetails.php b/Model/Api/AdyenPaymentsDetails.php index 5691a359a6..2f2996539c 100644 --- a/Model/Api/AdyenPaymentsDetails.php +++ b/Model/Api/AdyenPaymentsDetails.php @@ -75,7 +75,7 @@ public function initiate(string $payload, string $orderId): string $this->paymentResponseHandler->formatPaymentResponse( $response['resultCode'], $response['action'] ?? null, - $response['additionalData'] ?? null + !empty($response['donationToken']) ) ); } diff --git a/Model/Api/GuestAdyenOrderPaymentStatus.php b/Model/Api/GuestAdyenOrderPaymentStatus.php index f652881dd2..477486714d 100644 --- a/Model/Api/GuestAdyenOrderPaymentStatus.php +++ b/Model/Api/GuestAdyenOrderPaymentStatus.php @@ -78,7 +78,7 @@ public function getOrderPaymentStatus(string $orderId, string $cartId): string return json_encode($this->paymentResponseHandler->formatPaymentResponse( $additionalInformation['resultCode'], !empty($additionalInformation['action']) ? $additionalInformation['action'] : null, - !empty($additionalInformation['additionalData']) ? $additionalInformation['additionalData'] : null + !empty($additionalInformation['donationToken']) )); } } diff --git a/etc/schema.graphqls b/etc/schema.graphqls index cce10008b2..1e2a0aef41 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -46,6 +46,7 @@ type AdyenPaymentStatus { resultCode: String @doc(description: "Current state of the order in Adyen.") additionalData: String @doc(description: "Additional data required for the next step in the payment process.") action: String @doc(description: "Object containing information about the payment's next step.") + canDonate: Boolean @doc(description: "Indicates if donations are available for this order.") } type AdyenPaymentMethods { From c25c89adbd58da1bc866a0ce6eab1acc071f9fb0 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 17 Feb 2026 11:57:11 +0100 Subject: [PATCH 2/4] [ECP-8667] Implement GraphQl resolver for donation endpoints --- Model/GraphqlInputArgumentValidator.php | 53 ++++++++++ Model/Resolver/DonationCampaigns.php | 108 +++++++++++++++++++++ Model/Resolver/Donations.php | 124 ++++++++++++++++++++++++ etc/graphql/di.xml | 10 ++ etc/schema.graphqls | 23 +++++ 5 files changed, 318 insertions(+) create mode 100644 Model/GraphqlInputArgumentValidator.php create mode 100644 Model/Resolver/DonationCampaigns.php create mode 100644 Model/Resolver/Donations.php diff --git a/Model/GraphqlInputArgumentValidator.php b/Model/GraphqlInputArgumentValidator.php new file mode 100644 index 0000000000..ec2cabe0b4 --- /dev/null +++ b/Model/GraphqlInputArgumentValidator.php @@ -0,0 +1,53 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +class GraphqlInputArgumentValidator +{ + /** + * Validates GraphQl input arguments + * + * Multidimensional arrays can be validated with fields separated with a dot. + * + * @param array|null $args + * @param array $requiredFields + * @return void + * @throws GraphQlInputException + */ + public function execute(?array $args, array $requiredFields): void + { + $missingFields = []; + + foreach ($requiredFields as $field) { + $keys = explode('.', $field); + $value = $args; + + foreach ($keys as $key) { + $value = $value[$key] ?? null; + } + + if (empty($value)) { + $missingFields[] = $field; + } + } + + if (!empty($missingFields)) { + throw new GraphQlInputException( + __('Required parameters "%1" are missing', implode(', ', $missingFields)) + ); + } + } +} diff --git a/Model/Resolver/DonationCampaigns.php b/Model/Resolver/DonationCampaigns.php new file mode 100644 index 0000000000..4884efcc5a --- /dev/null +++ b/Model/Resolver/DonationCampaigns.php @@ -0,0 +1,108 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Api\AdyenDonationCampaigns; +use Adyen\Payment\Model\GraphqlInputArgumentValidator; +use Adyen\Payment\Model\Sales\OrderRepository; +use Exception; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; + +class DonationCampaigns implements ResolverInterface +{ + private const REQUIRED_FIELDS = [ + 'cartId' + ]; + + /** + * @param AdyenDonationCampaigns $adyenDonationCampaigns + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param OrderRepository $orderRepository + * @param GraphqlInputArgumentValidator $graphqlInputArgumentValidator + * @param AdyenLogger $adyenLogger + * @param AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + */ + public function __construct( + private readonly AdyenDonationCampaigns $adyenDonationCampaigns, + private readonly MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + private readonly OrderRepository $orderRepository, + private readonly GraphqlInputArgumentValidator $graphqlInputArgumentValidator, + private readonly AdyenLogger $adyenLogger, + private readonly AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + ) { } + + /** + * @param Field $field + * @param $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array + * @throws GraphQlAdyenException + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + ?array $value = null, + ?array $args = null + ): array { + $this->graphqlInputArgumentValidator->execute($args, self::REQUIRED_FIELDS); + + try { + $quoteId = $this->maskedQuoteIdToQuoteId->execute($args['cartId']); + } catch (NoSuchEntityException $e) { + $this->adyenLogger->error(sprintf("Quote with masked ID %s not found!", $args['cartId'])); + throw new GraphQlAdyenException(__('An error occurred while retrieving donation campaigns.')); + } + + $order = $this->orderRepository->getOrderByQuoteId($quoteId); + + if (!$order) { + $this->adyenLogger->error(sprintf("Order for quote ID %s not found!", $quoteId)); + throw new GraphQlAdyenException(__('An error occurred while retrieving donation campaigns.')); + } + + try { + $campaignsResponse = $this->adyenDonationCampaigns->getCampaigns((int) $order->getEntityId()); + } catch (LocalizedException $e) { + throw $this->adyenGraphQlExceptionMessageFormatter->getFormatted( + $e, + __('Unable to retrieve donation campaigns.'), + 'Unable to donate', + $field, + $context, + $info + ); + } catch (Exception $e) { + $this->adyenLogger->error(sprintf( + 'Unable to retrieve donation campaigns: %s', + $e->getMessage() + )); + throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); + } + + return ['campaignsData' => $campaignsResponse]; + } +} diff --git a/Model/Resolver/Donations.php b/Model/Resolver/Donations.php new file mode 100644 index 0000000000..3594344438 --- /dev/null +++ b/Model/Resolver/Donations.php @@ -0,0 +1,124 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Api\AdyenDonations; +use Adyen\Payment\Model\GraphqlInputArgumentValidator; +use Adyen\Payment\Model\Sales\OrderRepository; +use Exception; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; + +class Donations implements ResolverInterface +{ + private const REQUIRED_FIELDS = [ + 'cartId', + 'amount', + 'amount.currency', + 'returnUrl' + ]; + + /** + * @param AdyenDonations $adyenDonations + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param OrderRepository $orderRepository + * @param Json $jsonSerializer + * @param GraphqlInputArgumentValidator $graphqlInputArgumentValidator + * @param AdyenLogger $adyenLogger + * @param AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + */ + public function __construct( + private readonly AdyenDonations $adyenDonations, + private readonly MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + private readonly OrderRepository $orderRepository, + private readonly Json $jsonSerializer, + private readonly GraphqlInputArgumentValidator $graphqlInputArgumentValidator, + private readonly AdyenLogger $adyenLogger, + private readonly AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + ) { } + + /** + * @param Field $field + * @param $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array + * @throws GraphQlAdyenException + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + ?array $value = null, + ?array $args = null + ): array { + $this->graphqlInputArgumentValidator->execute($args, self::REQUIRED_FIELDS); + + try { + $quoteId = $this->maskedQuoteIdToQuoteId->execute($args['cartId']); + } catch (NoSuchEntityException $e) { + $this->adyenLogger->error(sprintf("Quote with masked ID %s not found!", $args['cartId'])); + throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); + } + + $order = $this->orderRepository->getOrderByQuoteId($quoteId); + + if (!$order) { + $this->adyenLogger->error(sprintf("Order for quote ID %s not found!", $quoteId)); + throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); + } + + $payloadData = [ + 'amount' => [ + 'currency' => $args['amount']['currency'], + 'value' => $args['amount']['value'] + ], + 'returnUrl' => $args['returnUrl'] + ]; + + $payload = $this->jsonSerializer->serialize($payloadData); + + try { + $this->adyenDonations->makeDonation($payload, $order); + } catch (LocalizedException $e) { + throw $this->adyenGraphQlExceptionMessageFormatter->getFormatted( + $e, + __('Donation failed!'), + 'Unable to donate', + $field, + $context, + $info + ); + } catch (Exception $e) { + $this->adyenLogger->error(sprintf( + 'GraphQl donation call failed with error message: %s', + $e->getMessage() + )); + throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); + } + + return ['status' => true]; + } +} diff --git a/etc/graphql/di.xml b/etc/graphql/di.xml index 08bc8e1a49..2da51ce653 100644 --- a/etc/graphql/di.xml +++ b/etc/graphql/di.xml @@ -171,4 +171,14 @@ AdyenGraphQlExceptionMessageFormatter + + + AdyenGraphQlExceptionMessageFormatter + + + + + AdyenGraphQlExceptionMessageFormatter + + diff --git a/etc/schema.graphqls b/etc/schema.graphqls index 1e2a0aef41..b9a4e56bbd 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -18,6 +18,10 @@ type Query { adyenRedeemedGiftcards ( cartId: String! @doc(description: "Cart ID for which to fetch redeemed gift cards.") ): AdyenRedeemedGiftcardsResponse @resolver(class: "Adyen\\Payment\\Model\\Resolver\\GetAdyenRedeemedGiftcards") + + adyenDonationCampaigns ( + cartId: String! @doc(description: "Cart ID associated with the donation") + ): AdyenDonationCampaignsResponse @resolver(class: "Adyen\\Payment\\Model\\Resolver\\DonationCampaigns") } type Mutation { @@ -39,6 +43,25 @@ type Mutation { posPayment( cartId: String! @doc(description: "Cart ID associated with the POS payment.") ): AdyenPaymentStatus @resolver(class: "Adyen\\Payment\\Model\\Resolver\\InitiatePosPayment") + + adyenDonate( + cartId: String! @doc(description: "Cart ID associated with the donation") + amount: AdyenAmountInput! @doc(description: "Donation amount") + returnUrl: String! @doc(description: "The URL to return to after the donation is completed in case of a redirect") + ): AdyenDonationStatus @resolver(class: "Adyen\\Payment\\Model\\Resolver\\Donations") +} + +input AdyenAmountInput { + currency: String! @doc(description: "Three-letter ISO currency code, e.g. EUR.") + value: Int! @doc(description: "Amount in minor units, e.g. 500 for 5.00 EUR.") +} + +type AdyenDonationStatus { + status: Boolean @doc(description: "True if the donation was processed successfully.") +} + +type AdyenDonationCampaignsResponse { + campaignsData: String @doc(description: "JSON encoded donation campaigns data.") } type AdyenPaymentStatus { From a0f94f7d0c5065bd6d378328b9442dd03b0b2c72 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Tue, 17 Feb 2026 13:17:23 +0100 Subject: [PATCH 3/4] [ECP-8667] Write unit tests --- .../Helper/PaymentResponseHandlerTest.php | 26 ++ .../GraphqlInputArgumentValidatorTest.php | 172 ++++++++++ .../Model/Resolver/DonationCampaignsTest.php | 252 +++++++++++++++ Test/Unit/Model/Resolver/DonationsTest.php | 294 ++++++++++++++++++ 4 files changed, 744 insertions(+) create mode 100644 Test/Unit/Model/GraphqlInputArgumentValidatorTest.php create mode 100644 Test/Unit/Model/Resolver/DonationCampaignsTest.php create mode 100644 Test/Unit/Model/Resolver/DonationsTest.php diff --git a/Test/Unit/Helper/PaymentResponseHandlerTest.php b/Test/Unit/Helper/PaymentResponseHandlerTest.php index 5579bb48ba..738391bc9c 100644 --- a/Test/Unit/Helper/PaymentResponseHandlerTest.php +++ b/Test/Unit/Helper/PaymentResponseHandlerTest.php @@ -118,6 +118,10 @@ public function testFormatPaymentResponseForFinalResultCodes($resultCode) "resultCode" => $resultCode ]; + if ($resultCode === PaymentResponseHandler::AUTHORISED) { + $expectedResult["canDonate"] = false; + } + // Execute method of the tested class $result = $this->paymentResponseHandler->formatPaymentResponse($resultCode); @@ -125,6 +129,28 @@ public function testFormatPaymentResponseForFinalResultCodes($resultCode) $this->assertEquals($expectedResult, $result); } + /** + * @return void + */ + public function testFormatPaymentResponseForAuthorisedWithDonationToken() + { + $expectedResult = [ + "isFinal" => true, + "resultCode" => PaymentResponseHandler::AUTHORISED, + "canDonate" => true + ]; + + // Execute method of the tested class + $result = $this->paymentResponseHandler->formatPaymentResponse( + PaymentResponseHandler::AUTHORISED, + null, + true + ); + + // Assert conditions + $this->assertEquals($expectedResult, $result); + } + public static function dataSourceForFormatPaymentResponseActionRequiredPayments(): array { return [ diff --git a/Test/Unit/Model/GraphqlInputArgumentValidatorTest.php b/Test/Unit/Model/GraphqlInputArgumentValidatorTest.php new file mode 100644 index 0000000000..d10716acf3 --- /dev/null +++ b/Test/Unit/Model/GraphqlInputArgumentValidatorTest.php @@ -0,0 +1,172 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Test\Unit\Model; + +use Adyen\Payment\Model\GraphqlInputArgumentValidator; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +class GraphqlInputArgumentValidatorTest extends AbstractAdyenTestCase +{ + private GraphqlInputArgumentValidator $validator; + + protected function setUp(): void + { + $this->validator = new GraphqlInputArgumentValidator(); + } + + /** + * @return void + * @throws GraphQlInputException + */ + public function testExecuteWithAllRequiredFieldsPresent() + { + $args = [ + 'name' => 'John', + 'email' => 'john@example.com' + ]; + $requiredFields = ['name', 'email']; + + $this->validator->execute($args, $requiredFields); + + // No exception means the test passes + $this->assertTrue(true); + } + + /** + * @return void + * @throws GraphQlInputException + */ + public function testExecuteWithNestedRequiredFieldsPresent() + { + $args = [ + 'payment' => [ + 'method' => 'adyen_cc', + 'details' => [ + 'brand' => 'visa' + ] + ] + ]; + $requiredFields = ['payment.method', 'payment.details.brand']; + + $this->validator->execute($args, $requiredFields); + + $this->assertTrue(true); + } + + /** + * @return void + */ + public function testExecuteThrowsExceptionForMissingTopLevelField() + { + $args = [ + 'name' => 'John' + ]; + $requiredFields = ['name', 'email']; + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('email'); + + $this->validator->execute($args, $requiredFields); + } + + /** + * @return void + */ + public function testExecuteThrowsExceptionForMissingNestedField() + { + $args = [ + 'payment' => [ + 'method' => 'adyen_cc' + ] + ]; + $requiredFields = ['payment.details.brand']; + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('payment.details.brand'); + + $this->validator->execute($args, $requiredFields); + } + + /** + * @return void + */ + public function testExecuteThrowsExceptionWithMultipleMissingFields() + { + $args = [ + 'name' => 'John' + ]; + $requiredFields = ['email', 'phone']; + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('email, phone'); + + $this->validator->execute($args, $requiredFields); + } + + /** + * @return void + */ + public function testExecuteThrowsExceptionForNullArgs() + { + $requiredFields = ['name']; + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('name'); + + $this->validator->execute(null, $requiredFields); + } + + /** + * @return void + */ + public function testExecuteThrowsExceptionForEmptyArgs() + { + $requiredFields = ['name']; + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('name'); + + $this->validator->execute([], $requiredFields); + } + + /** + * @return void + * @throws GraphQlInputException + */ + public function testExecuteWithNoRequiredFields() + { + $args = ['name' => 'John']; + + $this->validator->execute($args, []); + + $this->assertTrue(true); + } + + /** + * @return void + */ + public function testExecuteThrowsExceptionForEmptyStringValue() + { + $args = [ + 'name' => '' + ]; + $requiredFields = ['name']; + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('name'); + + $this->validator->execute($args, $requiredFields); + } +} diff --git a/Test/Unit/Model/Resolver/DonationCampaignsTest.php b/Test/Unit/Model/Resolver/DonationCampaignsTest.php new file mode 100644 index 0000000000..f06a3b26eb --- /dev/null +++ b/Test/Unit/Model/Resolver/DonationCampaignsTest.php @@ -0,0 +1,252 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Test\Unit\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Api\AdyenDonationCampaigns; +use Adyen\Payment\Model\GraphqlInputArgumentValidator; +use Adyen\Payment\Model\Resolver\DonationCampaigns; +use Adyen\Payment\Model\Sales\OrderRepository; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Exception; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Sales\Model\Order; +use PHPUnit\Framework\MockObject\MockObject; + +class DonationCampaignsTest extends AbstractAdyenTestCase +{ + private MockObject $adyenDonationCampaignsMock; + private MockObject $maskedQuoteIdToQuoteIdMock; + private MockObject $orderRepositoryMock; + private MockObject $graphqlInputArgumentValidatorMock; + private MockObject $adyenLoggerMock; + private MockObject $adyenGraphQlExceptionMessageFormatterMock; + private MockObject $fieldMock; + private MockObject $contextMock; + private MockObject $infoMock; + private DonationCampaigns $donationCampaignsResolver; + + protected function setUp(): void + { + $this->adyenDonationCampaignsMock = $this->createMock(AdyenDonationCampaigns::class); + $this->maskedQuoteIdToQuoteIdMock = $this->createMock(MaskedQuoteIdToQuoteIdInterface::class); + $this->orderRepositoryMock = $this->createMock(OrderRepository::class); + $this->graphqlInputArgumentValidatorMock = $this->createMock(GraphqlInputArgumentValidator::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + $this->adyenGraphQlExceptionMessageFormatterMock = $this->createMock(AggregateExceptionMessageFormatter::class); + $this->fieldMock = $this->createMock(Field::class); + $this->contextMock = $this->createMock(ContextInterface::class); + $this->infoMock = $this->createMock(ResolveInfo::class); + + $this->donationCampaignsResolver = new DonationCampaigns( + $this->adyenDonationCampaignsMock, + $this->maskedQuoteIdToQuoteIdMock, + $this->orderRepositoryMock, + $this->graphqlInputArgumentValidatorMock, + $this->adyenLoggerMock, + $this->adyenGraphQlExceptionMessageFormatterMock + ); + } + + public function testResolveSuccessful(): void + { + $args = ['cartId' => 'masked_cart_id']; + $quoteId = 123; + $orderId = 456; + $campaignsResponse = '{"campaigns": [{"id": "1"}]}'; + + $orderMock = $this->createMock(Order::class); + $orderMock->method('getEntityId')->willReturn($orderId); + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn($orderMock); + + $this->adyenDonationCampaignsMock->expects($this->once()) + ->method('getCampaigns') + ->with($orderId) + ->willReturn($campaignsResponse); + + $result = $this->donationCampaignsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + + $this->assertEquals(['campaignsData' => $campaignsResponse], $result); + } + + public function testResolveThrowsExceptionForMissingRequiredFields(): void + { + $args = []; + + $this->graphqlInputArgumentValidatorMock->method('execute') + ->willThrowException(new GraphQlInputException(__('Required parameters "cartId" are missing'))); + + $this->expectException(GraphQlInputException::class); + + $this->donationCampaignsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsExceptionWhenQuoteNotFound(): void + { + $args = ['cartId' => 'invalid_masked_id']; + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('invalid_masked_id') + ->willThrowException(new NoSuchEntityException()); + + $this->adyenLoggerMock->expects($this->once())->method('error'); + + $this->expectException(GraphQlAdyenException::class); + + $this->donationCampaignsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsExceptionWhenOrderNotFound(): void + { + $args = ['cartId' => 'masked_cart_id']; + $quoteId = 123; + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn(null); + + $this->adyenLoggerMock->expects($this->once())->method('error'); + + $this->expectException(GraphQlAdyenException::class); + + $this->donationCampaignsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsFormattedExceptionOnLocalizedException(): void + { + $args = ['cartId' => 'masked_cart_id']; + $quoteId = 123; + $orderId = 456; + + $orderMock = $this->createMock(Order::class); + $orderMock->method('getEntityId')->willReturn($orderId); + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn($orderMock); + + $localizedException = new LocalizedException(__('Unable to retrieve donation campaigns.')); + + $this->adyenDonationCampaignsMock->method('getCampaigns') + ->with($orderId) + ->willThrowException($localizedException); + + $formattedException = new GraphQlInputException(__('Unable to retrieve donation campaigns.')); + + $this->adyenGraphQlExceptionMessageFormatterMock + ->expects($this->once()) + ->method('getFormatted') + ->with( + $this->equalTo($localizedException), + $this->anything(), + $this->equalTo('Unable to donate'), + $this->equalTo($this->fieldMock), + $this->equalTo($this->contextMock), + $this->equalTo($this->infoMock) + ) + ->willReturn($formattedException); + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('Unable to retrieve donation campaigns.'); + + $this->donationCampaignsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsAdyenExceptionOnGenericException(): void + { + $args = ['cartId' => 'masked_cart_id']; + $quoteId = 123; + $orderId = 456; + + $orderMock = $this->createMock(Order::class); + $orderMock->method('getEntityId')->willReturn($orderId); + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn($orderMock); + + $this->adyenDonationCampaignsMock->method('getCampaigns') + ->with($orderId) + ->willThrowException(new Exception('Unexpected error')); + + $this->adyenLoggerMock->expects($this->once())->method('error'); + + $this->expectException(GraphQlAdyenException::class); + + $this->donationCampaignsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } +} diff --git a/Test/Unit/Model/Resolver/DonationsTest.php b/Test/Unit/Model/Resolver/DonationsTest.php new file mode 100644 index 0000000000..c18acbc12e --- /dev/null +++ b/Test/Unit/Model/Resolver/DonationsTest.php @@ -0,0 +1,294 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Test\Unit\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Api\AdyenDonations; +use Adyen\Payment\Model\GraphqlInputArgumentValidator; +use Adyen\Payment\Model\Resolver\Donations; +use Adyen\Payment\Model\Sales\OrderRepository; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Exception; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Sales\Model\Order; +use PHPUnit\Framework\MockObject\MockObject; + +class DonationsTest extends AbstractAdyenTestCase +{ + private MockObject $adyenDonationsMock; + private MockObject $maskedQuoteIdToQuoteIdMock; + private MockObject $orderRepositoryMock; + private MockObject $jsonSerializerMock; + private MockObject $graphqlInputArgumentValidatorMock; + private MockObject $adyenLoggerMock; + private MockObject $adyenGraphQlExceptionMessageFormatterMock; + private MockObject $fieldMock; + private MockObject $contextMock; + private MockObject $infoMock; + private Donations $donationsResolver; + + protected function setUp(): void + { + $this->adyenDonationsMock = $this->createMock(AdyenDonations::class); + $this->maskedQuoteIdToQuoteIdMock = $this->createMock(MaskedQuoteIdToQuoteIdInterface::class); + $this->orderRepositoryMock = $this->createMock(OrderRepository::class); + $this->jsonSerializerMock = $this->createMock(Json::class); + $this->graphqlInputArgumentValidatorMock = $this->createMock(GraphqlInputArgumentValidator::class); + $this->adyenLoggerMock = $this->createMock(AdyenLogger::class); + $this->adyenGraphQlExceptionMessageFormatterMock = $this->createMock(AggregateExceptionMessageFormatter::class); + $this->fieldMock = $this->createMock(Field::class); + $this->contextMock = $this->createMock(ContextInterface::class); + $this->infoMock = $this->createMock(ResolveInfo::class); + + $this->donationsResolver = new Donations( + $this->adyenDonationsMock, + $this->maskedQuoteIdToQuoteIdMock, + $this->orderRepositoryMock, + $this->jsonSerializerMock, + $this->graphqlInputArgumentValidatorMock, + $this->adyenLoggerMock, + $this->adyenGraphQlExceptionMessageFormatterMock + ); + } + + public function testResolveSuccessful(): void + { + $args = [ + 'cartId' => 'masked_cart_id', + 'amount' => [ + 'currency' => 'EUR', + 'value' => 500 + ], + 'returnUrl' => 'https://example.com/return' + ]; + + $quoteId = 123; + $orderMock = $this->createMock(Order::class); + $serializedPayload = '{"amount":{"currency":"EUR","value":500},"returnUrl":"https:\/\/example.com\/return"}'; + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn($orderMock); + + $this->jsonSerializerMock->method('serialize') + ->willReturn($serializedPayload); + + $this->adyenDonationsMock->expects($this->once()) + ->method('makeDonation') + ->with($serializedPayload, $orderMock); + + $result = $this->donationsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + + $this->assertEquals(['status' => true], $result); + } + + public function testResolveThrowsExceptionForMissingRequiredFields(): void + { + $args = [ + 'cartId' => 'masked_cart_id' + ]; + + $this->graphqlInputArgumentValidatorMock->method('execute') + ->willThrowException(new GraphQlInputException(__('Required parameters "amount, returnUrl" are missing'))); + + $this->expectException(GraphQlInputException::class); + + $this->donationsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsExceptionWhenQuoteNotFound(): void + { + $args = [ + 'cartId' => 'invalid_masked_id', + 'amount' => [ + 'currency' => 'EUR', + 'value' => 500 + ], + 'returnUrl' => 'https://example.com/return' + ]; + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('invalid_masked_id') + ->willThrowException(new NoSuchEntityException()); + + $this->adyenLoggerMock->expects($this->once())->method('error'); + + $this->expectException(GraphQlAdyenException::class); + + $this->donationsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsExceptionWhenOrderNotFound(): void + { + $args = [ + 'cartId' => 'masked_cart_id', + 'amount' => [ + 'currency' => 'EUR', + 'value' => 500 + ], + 'returnUrl' => 'https://example.com/return' + ]; + + $quoteId = 123; + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn(null); + + $this->adyenLoggerMock->expects($this->once())->method('error'); + + $this->expectException(GraphQlAdyenException::class); + + $this->donationsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsFormattedExceptionOnLocalizedException(): void + { + $args = [ + 'cartId' => 'masked_cart_id', + 'amount' => [ + 'currency' => 'EUR', + 'value' => 500 + ], + 'returnUrl' => 'https://example.com/return' + ]; + + $quoteId = 123; + $orderMock = $this->createMock(Order::class); + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn($orderMock); + + $this->jsonSerializerMock->method('serialize') + ->willReturn('{}'); + + $localizedException = new LocalizedException(__('Donation failed!')); + + $this->adyenDonationsMock->method('makeDonation') + ->willThrowException($localizedException); + + $formattedException = new GraphQlInputException(__('Donation failed!')); + + $this->adyenGraphQlExceptionMessageFormatterMock + ->expects($this->once()) + ->method('getFormatted') + ->with( + $this->equalTo($localizedException), + $this->anything(), + $this->equalTo('Unable to donate'), + $this->equalTo($this->fieldMock), + $this->equalTo($this->contextMock), + $this->equalTo($this->infoMock) + ) + ->willReturn($formattedException); + + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('Donation failed!'); + + $this->donationsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveThrowsAdyenExceptionOnGenericException(): void + { + $args = [ + 'cartId' => 'masked_cart_id', + 'amount' => [ + 'currency' => 'EUR', + 'value' => 500 + ], + 'returnUrl' => 'https://example.com/return' + ]; + + $quoteId = 123; + $orderMock = $this->createMock(Order::class); + + $this->maskedQuoteIdToQuoteIdMock->method('execute') + ->with('masked_cart_id') + ->willReturn($quoteId); + + $this->orderRepositoryMock->method('getOrderByQuoteId') + ->with($quoteId) + ->willReturn($orderMock); + + $this->jsonSerializerMock->method('serialize') + ->willReturn('{}'); + + $this->adyenDonationsMock->method('makeDonation') + ->willThrowException(new Exception('Unexpected error')); + + $this->adyenLoggerMock->expects($this->once())->method('error'); + + $this->expectException(GraphQlAdyenException::class); + + $this->donationsResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } +} From 8262e3153a23cfe09719a77b66518b7d10706c8c Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Mon, 23 Feb 2026 10:01:38 +0100 Subject: [PATCH 4/4] [ECP-8667] Move duplicate code to the abstract class --- Model/Resolver/AbstractDonationResolver.php | 131 ++++++++++++++++++ Model/Resolver/DonationCampaigns.php | 89 +++--------- Model/Resolver/Donations.php | 100 ++++--------- .../Model/Resolver/DonationCampaignsTest.php | 15 +- Test/Unit/Model/Resolver/DonationsTest.php | 17 ++- 5 files changed, 197 insertions(+), 155 deletions(-) create mode 100644 Model/Resolver/AbstractDonationResolver.php diff --git a/Model/Resolver/AbstractDonationResolver.php b/Model/Resolver/AbstractDonationResolver.php new file mode 100644 index 0000000000..1fe1a26952 --- /dev/null +++ b/Model/Resolver/AbstractDonationResolver.php @@ -0,0 +1,131 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\GraphqlInputArgumentValidator; +use Adyen\Payment\Model\Sales\OrderRepository; +use Exception; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Sales\Api\Data\OrderInterface; + +abstract class AbstractDonationResolver implements ResolverInterface +{ + /** + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param OrderRepository $orderRepository + * @param GraphqlInputArgumentValidator $graphqlInputArgumentValidator + * @param AdyenLogger $adyenLogger + * @param AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + */ + public function __construct( + protected readonly MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + protected readonly OrderRepository $orderRepository, + protected readonly GraphqlInputArgumentValidator $graphqlInputArgumentValidator, + protected readonly AdyenLogger $adyenLogger, + protected readonly AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + ) { } + + /** + * @return array + */ + abstract protected function getRequiredFields(): array; + + /** + * @param OrderInterface $order + * @param array $args + * @param Field $field + * @param $context + * @param ResolveInfo $info + * @return array + * @throws GraphQlAdyenException + */ + abstract protected function performOperation( + OrderInterface $order, + array $args, + Field $field, + $context, + ResolveInfo $info + ): array; + + /** + * @return string + */ + protected function getGenericErrorMessage(): string + { + return 'An error occurred while processing the donation.'; + } + + /** + * @param Field $field + * @param $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array + * @throws GraphQlAdyenException + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + ?array $value = null, + ?array $args = null + ): array { + $this->graphqlInputArgumentValidator->execute($args, $this->getRequiredFields()); + + try { + $quoteId = $this->maskedQuoteIdToQuoteId->execute($args['cartId']); + } catch (NoSuchEntityException $e) { + $this->adyenLogger->error(sprintf("Quote with masked ID %s not found!", $args['cartId'])); + throw new GraphQlAdyenException(__($this->getGenericErrorMessage())); + } + + $order = $this->orderRepository->getOrderByQuoteId($quoteId); + + if (!$order) { + $this->adyenLogger->error(sprintf("Order for quote ID %s not found!", $quoteId)); + throw new GraphQlAdyenException(__($this->getGenericErrorMessage())); + } + + try { + return $this->performOperation($order, $args, $field, $context, $info); + } catch (LocalizedException $e) { + throw $this->adyenGraphQlExceptionMessageFormatter->getFormatted( + $e, + __($this->getGenericErrorMessage()), + $this->getGenericErrorMessage(), + $field, + $context, + $info + ); + } catch (Exception $e) { + $this->adyenLogger->error(sprintf( + '%s: %s', + $this->getGenericErrorMessage(), + $e->getMessage() + )); + throw new GraphQlAdyenException(__($this->getGenericErrorMessage())); + } + } +} diff --git a/Model/Resolver/DonationCampaigns.php b/Model/Resolver/DonationCampaigns.php index 4884efcc5a..fb4cd62c3d 100644 --- a/Model/Resolver/DonationCampaigns.php +++ b/Model/Resolver/DonationCampaigns.php @@ -14,94 +14,43 @@ namespace Adyen\Payment\Model\Resolver; use Adyen\Payment\Exception\GraphQlAdyenException; -use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\Api\AdyenDonationCampaigns; -use Adyen\Payment\Model\GraphqlInputArgumentValidator; -use Adyen\Payment\Model\Sales\OrderRepository; -use Exception; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; -use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Sales\Api\Data\OrderInterface; -class DonationCampaigns implements ResolverInterface +class DonationCampaigns extends AbstractDonationResolver { - private const REQUIRED_FIELDS = [ - 'cartId' - ]; - /** - * @param AdyenDonationCampaigns $adyenDonationCampaigns - * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId - * @param OrderRepository $orderRepository - * @param GraphqlInputArgumentValidator $graphqlInputArgumentValidator - * @param AdyenLogger $adyenLogger - * @param AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + * @return array */ - public function __construct( - private readonly AdyenDonationCampaigns $adyenDonationCampaigns, - private readonly MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, - private readonly OrderRepository $orderRepository, - private readonly GraphqlInputArgumentValidator $graphqlInputArgumentValidator, - private readonly AdyenLogger $adyenLogger, - private readonly AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter - ) { } + protected function getRequiredFields(): array + { + return [ + 'cartId' + ]; + } /** + * @param OrderInterface $order + * @param array $args * @param Field $field * @param $context * @param ResolveInfo $info - * @param array|null $value - * @param array|null $args * @return array - * @throws GraphQlAdyenException - * @throws GraphQlInputException + * @throws GraphQlAdyenException|LocalizedException */ - public function resolve( + protected function performOperation( + OrderInterface $order, + array $args, Field $field, $context, - ResolveInfo $info, - ?array $value = null, - ?array $args = null + ResolveInfo $info ): array { - $this->graphqlInputArgumentValidator->execute($args, self::REQUIRED_FIELDS); - - try { - $quoteId = $this->maskedQuoteIdToQuoteId->execute($args['cartId']); - } catch (NoSuchEntityException $e) { - $this->adyenLogger->error(sprintf("Quote with masked ID %s not found!", $args['cartId'])); - throw new GraphQlAdyenException(__('An error occurred while retrieving donation campaigns.')); - } - - $order = $this->orderRepository->getOrderByQuoteId($quoteId); - - if (!$order) { - $this->adyenLogger->error(sprintf("Order for quote ID %s not found!", $quoteId)); - throw new GraphQlAdyenException(__('An error occurred while retrieving donation campaigns.')); - } - - try { - $campaignsResponse = $this->adyenDonationCampaigns->getCampaigns((int) $order->getEntityId()); - } catch (LocalizedException $e) { - throw $this->adyenGraphQlExceptionMessageFormatter->getFormatted( - $e, - __('Unable to retrieve donation campaigns.'), - 'Unable to donate', - $field, - $context, - $info - ); - } catch (Exception $e) { - $this->adyenLogger->error(sprintf( - 'Unable to retrieve donation campaigns: %s', - $e->getMessage() - )); - throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); - } + $adyenDonationCampaigns = ObjectManager::getInstance()->get(AdyenDonationCampaigns::class); + $campaignsResponse = $adyenDonationCampaigns->getCampaigns((int) $order->getEntityId()); return ['campaignsData' => $campaignsResponse]; } diff --git a/Model/Resolver/Donations.php b/Model/Resolver/Donations.php index 3594344438..b9df12cfe2 100644 --- a/Model/Resolver/Donations.php +++ b/Model/Resolver/Donations.php @@ -14,82 +14,45 @@ namespace Adyen\Payment\Model\Resolver; use Adyen\Payment\Exception\GraphQlAdyenException; -use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\Api\AdyenDonations; -use Adyen\Payment\Model\GraphqlInputArgumentValidator; -use Adyen\Payment\Model\Sales\OrderRepository; -use Exception; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; -use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; +use Magento\Sales\Api\Data\OrderInterface; -class Donations implements ResolverInterface +class Donations extends AbstractDonationResolver { - private const REQUIRED_FIELDS = [ - 'cartId', - 'amount', - 'amount.currency', - 'returnUrl' - ]; - /** - * @param AdyenDonations $adyenDonations - * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId - * @param OrderRepository $orderRepository - * @param Json $jsonSerializer - * @param GraphqlInputArgumentValidator $graphqlInputArgumentValidator - * @param AdyenLogger $adyenLogger - * @param AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter + * @return array */ - public function __construct( - private readonly AdyenDonations $adyenDonations, - private readonly MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, - private readonly OrderRepository $orderRepository, - private readonly Json $jsonSerializer, - private readonly GraphqlInputArgumentValidator $graphqlInputArgumentValidator, - private readonly AdyenLogger $adyenLogger, - private readonly AggregateExceptionMessageFormatter $adyenGraphQlExceptionMessageFormatter - ) { } + protected function getRequiredFields(): array + { + return [ + 'cartId', + 'amount', + 'amount.currency', + 'returnUrl' + ]; + } /** + * @param OrderInterface $order + * @param array $args * @param Field $field * @param $context * @param ResolveInfo $info - * @param array|null $value - * @param array|null $args * @return array - * @throws GraphQlAdyenException - * @throws GraphQlInputException + * @throws GraphQlAdyenException|LocalizedException */ - public function resolve( + protected function performOperation( + OrderInterface $order, + array $args, Field $field, $context, - ResolveInfo $info, - ?array $value = null, - ?array $args = null + ResolveInfo $info ): array { - $this->graphqlInputArgumentValidator->execute($args, self::REQUIRED_FIELDS); - - try { - $quoteId = $this->maskedQuoteIdToQuoteId->execute($args['cartId']); - } catch (NoSuchEntityException $e) { - $this->adyenLogger->error(sprintf("Quote with masked ID %s not found!", $args['cartId'])); - throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); - } - - $order = $this->orderRepository->getOrderByQuoteId($quoteId); - - if (!$order) { - $this->adyenLogger->error(sprintf("Order for quote ID %s not found!", $quoteId)); - throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); - } - $payloadData = [ 'amount' => [ 'currency' => $args['amount']['currency'], @@ -98,26 +61,11 @@ public function resolve( 'returnUrl' => $args['returnUrl'] ]; - $payload = $this->jsonSerializer->serialize($payloadData); + $jsonSerializer = ObjectManager::getInstance()->get(Json::class); + $payload = $jsonSerializer->serialize($payloadData); - try { - $this->adyenDonations->makeDonation($payload, $order); - } catch (LocalizedException $e) { - throw $this->adyenGraphQlExceptionMessageFormatter->getFormatted( - $e, - __('Donation failed!'), - 'Unable to donate', - $field, - $context, - $info - ); - } catch (Exception $e) { - $this->adyenLogger->error(sprintf( - 'GraphQl donation call failed with error message: %s', - $e->getMessage() - )); - throw new GraphQlAdyenException(__('An error occurred while processing the donation.')); - } + $adyenDonations = ObjectManager::getInstance()->get(AdyenDonations::class); + $adyenDonations->makeDonation($payload, $order); return ['status' => true]; } diff --git a/Test/Unit/Model/Resolver/DonationCampaignsTest.php b/Test/Unit/Model/Resolver/DonationCampaignsTest.php index f06a3b26eb..2fed44bdb5 100644 --- a/Test/Unit/Model/Resolver/DonationCampaignsTest.php +++ b/Test/Unit/Model/Resolver/DonationCampaignsTest.php @@ -21,12 +21,14 @@ use Adyen\Payment\Model\Sales\OrderRepository; use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; use Exception; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManagerInterface; use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Magento\Sales\Model\Order; @@ -57,8 +59,13 @@ protected function setUp(): void $this->contextMock = $this->createMock(ContextInterface::class); $this->infoMock = $this->createMock(ResolveInfo::class); + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $objectManagerMock->method('get')->willReturnMap([ + [AdyenDonationCampaigns::class, $this->adyenDonationCampaignsMock] + ]); + ObjectManager::setInstance($objectManagerMock); + $this->donationCampaignsResolver = new DonationCampaigns( - $this->adyenDonationCampaignsMock, $this->maskedQuoteIdToQuoteIdMock, $this->orderRepositoryMock, $this->graphqlInputArgumentValidatorMock, @@ -189,7 +196,7 @@ public function testResolveThrowsFormattedExceptionOnLocalizedException(): void ->with($orderId) ->willThrowException($localizedException); - $formattedException = new GraphQlInputException(__('Unable to retrieve donation campaigns.')); + $formattedException = new GraphQlInputException(__('An error occurred while processing the donation.')); $this->adyenGraphQlExceptionMessageFormatterMock ->expects($this->once()) @@ -197,7 +204,7 @@ public function testResolveThrowsFormattedExceptionOnLocalizedException(): void ->with( $this->equalTo($localizedException), $this->anything(), - $this->equalTo('Unable to donate'), + $this->equalTo('An error occurred while processing the donation.'), $this->equalTo($this->fieldMock), $this->equalTo($this->contextMock), $this->equalTo($this->infoMock) @@ -205,7 +212,7 @@ public function testResolveThrowsFormattedExceptionOnLocalizedException(): void ->willReturn($formattedException); $this->expectException(GraphQlInputException::class); - $this->expectExceptionMessage('Unable to retrieve donation campaigns.'); + $this->expectExceptionMessage('An error occurred while processing the donation.'); $this->donationCampaignsResolver->resolve( $this->fieldMock, diff --git a/Test/Unit/Model/Resolver/DonationsTest.php b/Test/Unit/Model/Resolver/DonationsTest.php index c18acbc12e..547739b348 100644 --- a/Test/Unit/Model/Resolver/DonationsTest.php +++ b/Test/Unit/Model/Resolver/DonationsTest.php @@ -21,12 +21,14 @@ use Adyen\Payment\Model\Sales\OrderRepository; use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; use Exception; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\GraphQl\Helper\Error\AggregateExceptionMessageFormatter; use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; @@ -60,11 +62,16 @@ protected function setUp(): void $this->contextMock = $this->createMock(ContextInterface::class); $this->infoMock = $this->createMock(ResolveInfo::class); + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $objectManagerMock->method('get')->willReturnMap([ + [Json::class, $this->jsonSerializerMock], + [AdyenDonations::class, $this->adyenDonationsMock] + ]); + ObjectManager::setInstance($objectManagerMock); + $this->donationsResolver = new Donations( - $this->adyenDonationsMock, $this->maskedQuoteIdToQuoteIdMock, $this->orderRepositoryMock, - $this->jsonSerializerMock, $this->graphqlInputArgumentValidatorMock, $this->adyenLoggerMock, $this->adyenGraphQlExceptionMessageFormatterMock @@ -224,7 +231,7 @@ public function testResolveThrowsFormattedExceptionOnLocalizedException(): void $this->adyenDonationsMock->method('makeDonation') ->willThrowException($localizedException); - $formattedException = new GraphQlInputException(__('Donation failed!')); + $formattedException = new GraphQlInputException(__('An error occurred while processing the donation.')); $this->adyenGraphQlExceptionMessageFormatterMock ->expects($this->once()) @@ -232,7 +239,7 @@ public function testResolveThrowsFormattedExceptionOnLocalizedException(): void ->with( $this->equalTo($localizedException), $this->anything(), - $this->equalTo('Unable to donate'), + $this->equalTo('An error occurred while processing the donation.'), $this->equalTo($this->fieldMock), $this->equalTo($this->contextMock), $this->equalTo($this->infoMock) @@ -240,7 +247,7 @@ public function testResolveThrowsFormattedExceptionOnLocalizedException(): void ->willReturn($formattedException); $this->expectException(GraphQlInputException::class); - $this->expectExceptionMessage('Donation failed!'); + $this->expectExceptionMessage('An error occurred while processing the donation.'); $this->donationsResolver->resolve( $this->fieldMock,