diff --git a/Api/AdyenStateDataInterface.php b/Api/AdyenStateDataInterface.php index 8dbf658e3..db86c55df 100644 --- a/Api/AdyenStateDataInterface.php +++ b/Api/AdyenStateDataInterface.php @@ -18,13 +18,15 @@ interface AdyenStateDataInterface { /** - * Persist the Adyen state data for the quote so it can be used in the payment request + * Persist the Adyen state data for the quote and returns the stateDataId. + * So it can be used in the payment request. + * * * @param string $stateData * @param int $cartId - * @return void + * @return int */ - public function save(string $stateData, int $cartId): void; + public function save(string $stateData, int $cartId): int; /** * Removes the Adyen state data with the given entity id diff --git a/Api/GuestAdyenStateDataInterface.php b/Api/GuestAdyenStateDataInterface.php index 199eebeb0..c3747e320 100644 --- a/Api/GuestAdyenStateDataInterface.php +++ b/Api/GuestAdyenStateDataInterface.php @@ -18,13 +18,14 @@ interface GuestAdyenStateDataInterface { /** - * Persist the Adyen state data for the quote so it can be used in the payment request + * Persist the Adyen state data for the quote and returns the stateDataId. + * So it can be used in the payment request. * * @param string $stateData * @param string $cartId - * @return void + * @return int */ - public function save(string $stateData, string $cartId): void; + public function save(string $stateData, string $cartId): int; /** * Removes the Adyen state data with the given entity id diff --git a/Helper/StateData.php b/Helper/StateData.php index 3a83fa8d2..a3675468c 100644 --- a/Helper/StateData.php +++ b/Helper/StateData.php @@ -16,7 +16,9 @@ use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\ResourceModel\StateData as StateDataResourceModel; use Adyen\Payment\Model\ResourceModel\StateData\Collection as StateDataCollection; +use Adyen\Payment\Model\StateData as AdyenStateData; use Adyen\Payment\Model\StateDataFactory; +use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -80,7 +82,14 @@ public function getPaymentMethodVariant(int $quoteId): string return $stateDataByQuoteId['paymentMethod']['type']; } - public function saveStateData(string $stateData, int $quoteId): void + /** + * @param string $stateData + * @param int $quoteId + * @return AdyenStateData + * @throws LocalizedException + * @throws AlreadyExistsException + */ + public function saveStateData(string $stateData, int $quoteId): AdyenStateData { // Decode payload from frontend $stateData = json_decode($stateData, true); @@ -92,10 +101,11 @@ public function saveStateData(string $stateData, int $quoteId): void $stateData = json_encode($this->checkoutStateDataValidator->getValidatedAdditionalData($stateData)); - /** @var \Adyen\Payment\Model\StateData $stateDataObj */ $stateDataObj = $this->stateDataFactory->create(); $stateDataObj->setQuoteId($quoteId)->setStateData((string)$stateData); $this->stateDataResourceModel->save($stateDataObj); + + return $stateDataObj; } /** diff --git a/Model/Api/AdyenStateData.php b/Model/Api/AdyenStateData.php index 4233fe026..9559466a0 100644 --- a/Model/Api/AdyenStateData.php +++ b/Model/Api/AdyenStateData.php @@ -25,13 +25,14 @@ public function __construct( $this->stateDataHelper = $stateDataHelper; } - public function save(string $stateData, int $quoteId): void + public function save(string $stateData, int $cartId): int { - $this->stateDataHelper->saveStateData($stateData, $quoteId); + $stateData = $this->stateDataHelper->saveStateData($stateData, $cartId); + return $stateData->getEntityId(); } - public function remove(int $stateDataId, int $quoteId): bool + public function remove(int $stateDataId, int $cartId): bool { - return $this->stateDataHelper->removeStateData($stateDataId, $quoteId); + return $this->stateDataHelper->removeStateData($stateDataId, $cartId); } } diff --git a/Model/Api/GuestAdyenStateData.php b/Model/Api/GuestAdyenStateData.php index 0e2f02cfe..b78c9b2db 100644 --- a/Model/Api/GuestAdyenStateData.php +++ b/Model/Api/GuestAdyenStateData.php @@ -14,6 +14,7 @@ use Adyen\Payment\Api\GuestAdyenStateDataInterface; use Adyen\Payment\Helper\StateData as StateDataHelper; +use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -33,14 +34,19 @@ public function __construct( } /** + * @param string $stateData + * @param string $cartId + * @return int * @throws InputException * @throws LocalizedException + * @throws AlreadyExistsException */ - public function save(string $stateData, string $cartId): void + public function save(string $stateData, string $cartId): int { $quoteId = $this->getQuoteIdFromMaskedCartId($cartId); + $stateData = $this->stateDataHelper->saveStateData($stateData, $quoteId); - $this->stateDataHelper->saveStateData($stateData, $quoteId); + return $stateData->getEntityId(); } /** diff --git a/Model/Resolver/GetAdyenPaymentMethodsBalance.php b/Model/Resolver/GetAdyenPaymentMethodsBalance.php new file mode 100644 index 000000000..bd5baf4b6 --- /dev/null +++ b/Model/Resolver/GetAdyenPaymentMethodsBalance.php @@ -0,0 +1,76 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Model\Api\AdyenPaymentMethodsBalance; +use Exception; +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; + +class GetAdyenPaymentMethodsBalance implements ResolverInterface +{ + /** + * @var AdyenPaymentMethodsBalance + */ + private AdyenPaymentMethodsBalance $balance; + + /** + * @param AdyenPaymentMethodsBalance $balance + */ + public function __construct( + AdyenPaymentMethodsBalance $balance + ) { + $this->balance = $balance; + } + + /** + * @param Field $field + * @param $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array + * @throws GraphQlAdyenException + * @throws GraphQlInputException + * @throws Exception + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + if (empty($args['payload'])) { + throw new GraphQlInputException(__('Required parameter "payload" is missing')); + } + + $payload = $args['payload']; + try { + $balanceResponse = $this->balance->getBalance($payload); + } catch (Exception $e) { + throw new GraphQlAdyenException( + __('An error occurred while fetching the payment method balance.'), + $e + ); + } + + return ['balanceResponse' => $balanceResponse]; + } +} + + diff --git a/Model/Resolver/GetAdyenRedeemedGiftcards.php b/Model/Resolver/GetAdyenRedeemedGiftcards.php new file mode 100644 index 000000000..758174462 --- /dev/null +++ b/Model/Resolver/GetAdyenRedeemedGiftcards.php @@ -0,0 +1,99 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Helper\GiftcardPayment; +use Magento\Quote\Model\QuoteIdMaskFactory; +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; + +class GetAdyenRedeemedGiftcards implements ResolverInterface +{ + /** + * @var GiftcardPayment + */ + private GiftcardPayment $giftcardPayment; + + /** + * @var Json + */ + private Json $jsonSerializer; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @param GiftcardPayment $giftcardPayment + * @param Json $jsonSerializer + * @param QuoteIdMaskFactory $quoteIdMaskFactory + */ + public function __construct( + GiftcardPayment $giftcardPayment, + Json $jsonSerializer, + QuoteIdMaskFactory $quoteIdMaskFactory + ) { + $this->giftcardPayment = $giftcardPayment; + $this->jsonSerializer = $jsonSerializer; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + } + + /** + * @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 + ) { + if (empty($args['cartId'])) { + throw new GraphQlInputException(__('Required parameter "cartId" is missing')); + } + + $cartId = $args['cartId']; + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + $quoteId = $quoteIdMask->getQuoteId(); + $quoteId = (int)$quoteId; + + try { + $redeemedGiftcardsJson = $this->giftcardPayment->fetchRedeemedGiftcards($quoteId); + } catch (\Exception $e) { + throw new GraphQlAdyenException( + __('An error occurred while fetching redeemed gift cards: %1', $e->getMessage()) + ); + } + + $redeemedGiftcardsData = $this->jsonSerializer->unserialize($redeemedGiftcardsJson); + + return [ + 'redeemedGiftcards' => $redeemedGiftcardsData['redeemedGiftcards'], + 'remainingAmount' => $redeemedGiftcardsData['remainingAmount'], + 'totalDiscount' => $redeemedGiftcardsData['totalDiscount'] + ]; + } +} diff --git a/Model/Resolver/RemoveAdyenStateData.php b/Model/Resolver/RemoveAdyenStateData.php new file mode 100644 index 000000000..494dc36c2 --- /dev/null +++ b/Model/Resolver/RemoveAdyenStateData.php @@ -0,0 +1,93 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Model\Api\AdyenStateData; +use Exception; +use Magento\Framework\Exception\LocalizedException; +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\Quote\Model\QuoteIdMaskFactory; + +class RemoveAdyenStateData implements ResolverInterface +{ + /** + * @var AdyenStateData + */ + private AdyenStateData $adyenStateData; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @param AdyenStateData $adyenStateData + * @param QuoteIdMaskFactory $quoteIdMaskFactory + */ + public function __construct( + AdyenStateData $adyenStateData, + QuoteIdMaskFactory $quoteIdMaskFactory + ) { + $this->adyenStateData = $adyenStateData; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + } + + /** + * @param Field $field + * @param $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array + * @throws GraphQlAdyenException + * @throws GraphQlInputException + * @throws LocalizedException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + if (empty($args['stateDataId'])) { + throw new GraphQlInputException(__('Required parameter "stateDataId" is missing')); + } + + if (empty($args['cartId'])) { + throw new GraphQlInputException(__('Required parameter "cartId" is missing')); + } + + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($args['cartId'], 'masked_id'); + $quoteId = $quoteIdMask->getQuoteId(); + + try { + $result = $this->adyenStateData->remove((int) $args['stateDataId'], (int) $quoteId); + } catch (Exception $e) { + throw new GraphQlAdyenException(__('An error occurred while removing the state data.'), $e); + } + + if (!$result) { + throw new LocalizedException(__('An error occurred while removing the state data.')); + } + + return ['stateDataId' => $args['stateDataId']]; + } +} + + diff --git a/Model/Resolver/SaveAdyenStateData.php b/Model/Resolver/SaveAdyenStateData.php new file mode 100644 index 000000000..7fc1c2bee --- /dev/null +++ b/Model/Resolver/SaveAdyenStateData.php @@ -0,0 +1,87 @@ + + */ +declare(strict_types=1); + +namespace Adyen\Payment\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Model\Api\AdyenStateData; +use Exception; +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\Quote\Model\QuoteIdMaskFactory; + +class SaveAdyenStateData implements ResolverInterface +{ + /** + * @var AdyenStateData + */ + private AdyenStateData $adyenStateData; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @param AdyenStateData $adyenStateData + * @param QuoteIdMaskFactory $quoteIdMaskFactory + */ + public function __construct( + AdyenStateData $adyenStateData, + QuoteIdMaskFactory $quoteIdMaskFactory + ) { + $this->adyenStateData = $adyenStateData; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + } + + /** + * @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 { + if (empty($args['stateData'])) { + throw new GraphQlInputException(__('Required parameter "stateData" is missing')); + } + + if (empty($args['cartId'])) { + throw new GraphQlInputException(__('Required parameter "cartId" is missing')); + } + + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($args['cartId'], 'masked_id'); + $quoteId = $quoteIdMask->getQuoteId(); + + try { + $stateDataId = $this->adyenStateData->save($args['stateData'], (int) $quoteId); + } catch (Exception $e) { + throw new GraphQlAdyenException(__('An error occurred while saving the state data.'), $e); + } + + return ['stateDataId' => $stateDataId]; + } +} + + diff --git a/Observer/AdyenCcDataAssignObserver.php b/Observer/AdyenCcDataAssignObserver.php index c354be8fb..1d86fadc6 100644 --- a/Observer/AdyenCcDataAssignObserver.php +++ b/Observer/AdyenCcDataAssignObserver.php @@ -116,9 +116,8 @@ public function execute(Observer $observer) // JSON decode state data from the frontend or fetch it from the DB entity with the quote ID if (!empty($additionalData[self::STATE_DATA])) { $stateData = json_decode((string) $additionalData[self::STATE_DATA], true); - } else { - $stateData = $this->stateDataCollection->getStateDataArrayWithQuoteId($paymentInfo->getData('quote_id')); } + // Get validated state data array if (!empty($stateData)) { $stateData = $this->checkoutStateDataValidator->getValidatedAdditionalData($stateData); diff --git a/Observer/AdyenMotoDataAssignObserver.php b/Observer/AdyenMotoDataAssignObserver.php index d49c162bf..09be9b649 100644 --- a/Observer/AdyenMotoDataAssignObserver.php +++ b/Observer/AdyenMotoDataAssignObserver.php @@ -106,9 +106,8 @@ public function execute(Observer $observer) // JSON decode state data from the frontend or fetch it from the DB entity with the quote ID if (!empty($additionalData[self::STATE_DATA])) { $orderStateData = json_decode((string) $additionalData[self::STATE_DATA], true); - } else { - $orderStateData = $this->stateDataCollection->getStateDataArrayWithQuoteId($paymentInfo->getData('quote_id')); } + // Get validated state data array if (!empty($orderStateData)) { $orderStateData = $this->checkoutStateDataValidator->getValidatedAdditionalData($orderStateData); diff --git a/Observer/AdyenPaymentMethodDataAssignObserver.php b/Observer/AdyenPaymentMethodDataAssignObserver.php index 0c18603c5..3416158d1 100644 --- a/Observer/AdyenPaymentMethodDataAssignObserver.php +++ b/Observer/AdyenPaymentMethodDataAssignObserver.php @@ -89,9 +89,8 @@ public function execute(Observer $observer) } elseif (!empty($additionalData[self::CC_NUMBER])) { $stateData = json_decode((string) $additionalData[self::CC_NUMBER], true); $paymentInfo->setAdditionalInformation(self::BRAND_CODE, $stateData['paymentMethod']['type']); - } else { - $stateData = $this->stateDataCollection->getStateDataArrayWithQuoteId($paymentInfo->getData('quote_id')); } + // Get validated state data array if (!empty($stateData)) { $stateData = $this->checkoutStateDataValidator->getValidatedAdditionalData($stateData); diff --git a/Test/Unit/Model/Api/AdyenStateDataTest.php b/Test/Unit/Model/Api/AdyenStateDataTest.php new file mode 100644 index 000000000..ef8bddffb --- /dev/null +++ b/Test/Unit/Model/Api/AdyenStateDataTest.php @@ -0,0 +1,59 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Model\Api; + +use Adyen\Payment\Helper\StateData; +use Adyen\Payment\Model\Api\AdyenStateData; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class AdyenStateDataTest extends AbstractAdyenTestCase +{ + private $objectManager; + private $stateDataHelperMock; + private $adyenStateDataModel; + + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + + $this->stateDataHelperMock = $this->createMock(StateData::class); + + $this->adyenStateDataModel = $this->objectManager->getObject(AdyenStateData::class, [ + 'stateDataHelper' => $this->stateDataHelperMock + ]); + } + + public function testSaveSuccessful() + { + $stateData = '{"stateData":"dummyData"}'; + $cartId = 100; + + $stateDataMock = $this->createMock(\Adyen\Payment\Model\StateData::class); + $stateDataMock->method('getEntityId')->willReturn(1); + + $this->stateDataHelperMock->expects($this->once()) + ->method('saveStateData') + ->willReturn($stateDataMock); + + $this->adyenStateDataModel->save($stateData, $cartId); + } + + public function testRemoveSuccessful() + { + $stateDataId = 1; + $cartId = 100; + + $this->stateDataHelperMock->expects($this->once())->method('removeStateData'); + $this->adyenStateDataModel->remove($stateDataId, $cartId); + } +} diff --git a/Test/Unit/Model/Api/GuestAdyenStateDataTest.php b/Test/Unit/Model/Api/GuestAdyenStateDataTest.php index d2a9110a9..b440dc058 100644 --- a/Test/Unit/Model/Api/GuestAdyenStateDataTest.php +++ b/Test/Unit/Model/Api/GuestAdyenStateDataTest.php @@ -51,8 +51,11 @@ public function testSaveSuccessful() $stateData = '{"stateData":"dummyData"}'; $cartId = 'ABC123456789'; + $stateDataMock = $this->createMock(\Adyen\Payment\Model\StateData::class); + $stateDataMock->method('getEntityId')->willReturn(1); + $this->quoteIdMaskFactoryMock->method('create')->willReturn($this->quoteIdMaskMock); - $this->stateDataHelperMock->expects($this->once())->method('saveStateData'); + $this->stateDataHelperMock->expects($this->once())->method('saveStateData')->willReturn($stateDataMock); $this->guestAdyenStateDataModel->save($stateData, $cartId); } @@ -64,7 +67,6 @@ public function testRemoveSuccessful() $this->quoteIdMaskFactoryMock->method('create')->willReturn($this->quoteIdMaskMock); $this->stateDataHelperMock->expects($this->once())->method('removeStateData'); - $this->guestAdyenStateDataModel->remove($stateDataId, $cartId); } diff --git a/Test/Unit/Model/Resolver/GetAdyenPaymentMethodsBalanceTest.php b/Test/Unit/Model/Resolver/GetAdyenPaymentMethodsBalanceTest.php new file mode 100644 index 000000000..51002e804 --- /dev/null +++ b/Test/Unit/Model/Resolver/GetAdyenPaymentMethodsBalanceTest.php @@ -0,0 +1,84 @@ + + */ +namespace Adyen\Payment\Test\Model\Resolver; + +use Adyen\Payment\Model\Api\AdyenPaymentMethodsBalance; +use Adyen\Payment\Model\Resolver\GetAdyenPaymentMethodsBalance; +use Magento\Catalog\Model\Layer\ContextInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use PHPUnit\Framework\TestCase; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Adyen\Payment\Exception\GraphQlAdyenException; +use Magento\Framework\GraphQl\Query; + +class GetAdyenPaymentMethodsBalanceTest extends TestCase +{ + private $balanceMock; + private $contextMock; + private $fieldMock; + private $infoMock; + private $getAdyenPaymentMethodsBalance; + + protected function setUp(): void + { + $this->balanceMock = $this->createMock(AdyenPaymentMethodsBalance::class); + $this->contextMock = $this->createMock(ContextInterface::class); + $this->fieldMock = $this->createMock(Field::class); + $this->infoMock = $this->createMock(ResolveInfo::class); + + $this->getAdyenPaymentMethodsBalance = new GetAdyenPaymentMethodsBalance( + $this->balanceMock + ); + } + + public function testWithMissingPayloadArgument() + { + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage('Required parameter "payload" is missing'); + + $this->getAdyenPaymentMethodsBalance->resolve($this->fieldMock, $this->contextMock, $this->infoMock, [], []); + } + + public function testWithValidPayloadArgument() + { + $payload = '{\"paymentMethod\":{\"type\":\"giftcard\",\"brand\":\"svs\",\"encryptedCardNumber\":\"abc…\",\"encryptedSecurityCode\":\"xyz…\"},\"amount\":{\"currency\":\"EUR\",\"value\":1000}}'; + $args = ['payload' => $payload]; + $expectedBalanceResponse = '10'; + + $this->balanceMock->expects($this->once()) + ->method('getBalance') + ->with($payload) + ->willReturn($expectedBalanceResponse); + + $result = $this->getAdyenPaymentMethodsBalance->resolve($this->fieldMock, $this->contextMock, $this->infoMock, [], $args); + + $this->assertEquals(['balanceResponse' => $expectedBalanceResponse], $result); + } + + public function testWithFailingApiCall() + { + $this->expectException(GraphQlAdyenException::class); + + $args = [ + 'payload' => "{}" + ]; + + $this->balanceMock->method('getBalance')->willThrowException(new \Exception()); + + $this->getAdyenPaymentMethodsBalance->resolve($this->fieldMock, $this->contextMock, $this->infoMock, [], $args); + } +} + + + + + diff --git a/Test/Unit/Model/Resolver/GetAdyenRedeemedGiftcardsTest.php b/Test/Unit/Model/Resolver/GetAdyenRedeemedGiftcardsTest.php new file mode 100644 index 000000000..3f491ec4a --- /dev/null +++ b/Test/Unit/Model/Resolver/GetAdyenRedeemedGiftcardsTest.php @@ -0,0 +1,133 @@ + + */ +namespace Adyen\Payment\Test\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Helper\GiftcardPayment; +use Adyen\Payment\Model\Resolver\GetAdyenRedeemedGiftcards; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Exception; +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\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class GetAdyenRedeemedGiftcardsTest extends AbstractAdyenTestCase +{ + private $giftcardPaymentMock; + private $jsonSerializerMock; + private $quoteIdMaskFactoryMock; + private $quoteIdMaskMock; + private $getAdyenRedeemedGiftcards; + private $fieldMock; + private $contextMock; + private $resolveInfoMock; + + protected function setUp(): void + { + $this->giftcardPaymentMock = $this->createMock(GiftcardPayment::class); + $this->jsonSerializerMock = $this->createMock(Json::class); + $this->quoteIdMaskFactoryMock = $this->createGeneratedMock( + QuoteIdMaskFactory::class, + ['create'] + ); + $this->quoteIdMaskMock = $this->createMock(QuoteIdMask::class); + $this->quoteIdMaskFactoryMock->method('create')->willReturn($this->quoteIdMaskMock); + + $this->fieldMock = $this->createMock(Field::class); + $this->contextMock = $this->createMock(ContextInterface::class); + $this->resolveInfoMock = $this->createMock(ResolveInfo::class); + + $this->getAdyenRedeemedGiftcards = new GetAdyenRedeemedGiftcards( + $this->giftcardPaymentMock, + $this->jsonSerializerMock, + $this->quoteIdMaskFactoryMock + ); + } + + public function testSuccessfulRetrievalOfRedeemedGiftCardDetailsWithValidCartId() + { + $cartId = 'test_cart_id'; + $quoteId = 0; + $args = ['cartId' => $cartId]; + $redeemedGiftcardsJson = '{"redeemedGiftcards": [], "remainingAmount": 100, "totalDiscount": 10}'; + $redeemedGiftcardsData = json_decode($redeemedGiftcardsJson, true); + + $this->quoteIdMaskMock->expects($this->once()) + ->method('load') + ->with($cartId, 'masked_id') + ->willReturn($this->quoteIdMaskMock); + + $this->quoteIdMaskMock = $this->createGeneratedMock(QuoteIdMask::class, ['load', 'getQuoteId']); + $this->quoteIdMaskMock->method('load')->willReturn($this->quoteIdMaskMock); + $this->quoteIdMaskMock->method('getQuoteId')->willReturn(1); + + $this->giftcardPaymentMock->expects($this->once()) + ->method('fetchRedeemedGiftcards') + ->with($quoteId) + ->willReturn($redeemedGiftcardsJson); + + $this->jsonSerializerMock->expects($this->once()) + ->method('unserialize') + ->with($redeemedGiftcardsJson) + ->willReturn($redeemedGiftcardsData); + + $result = $this->getAdyenRedeemedGiftcards->resolve($this->fieldMock, $this->contextMock, $this->resolveInfoMock, [], $args); + + $this->assertEquals( + [ + 'redeemedGiftcards' => $redeemedGiftcardsData['redeemedGiftcards'], + 'remainingAmount' => $redeemedGiftcardsData['remainingAmount'], + 'totalDiscount' => $redeemedGiftcardsData['totalDiscount'] + ], + $result + ); + } + + public function testFailedRetrievalOfRedeemedGiftCards() + { + $this->expectException(GraphQlAdyenException::class); + + $cartId = 'test_cart_id'; + $args = ['cartId' => $cartId]; + + $this->quoteIdMaskMock->expects($this->once()) + ->method('load') + ->with($cartId, 'masked_id') + ->willReturn($this->quoteIdMaskMock); + + $this->quoteIdMaskMock = $this->createGeneratedMock(QuoteIdMask::class, ['load', 'getQuoteId']); + $this->quoteIdMaskMock->method('load')->willReturn($this->quoteIdMaskMock); + $this->quoteIdMaskMock->method('getQuoteId')->willReturn(1); + + $this->giftcardPaymentMock->method('fetchRedeemedGiftcards') + ->willThrowException(new Exception()); + + $this->getAdyenRedeemedGiftcards->resolve($this->fieldMock, $this->contextMock, $this->resolveInfoMock, [], $args); + } + + public function testFailedRetrievalOfRedeemedGiftCardsWithNullCartId() + { + $this->expectException(GraphQlInputException::class); + + $args = ['cartId' => ""]; + + $this->getAdyenRedeemedGiftcards->resolve($this->fieldMock, $this->contextMock, $this->resolveInfoMock, [], $args); + } +} + + + + + diff --git a/Test/Unit/Model/Resolver/RemoveAdyenStateDataTest.php b/Test/Unit/Model/Resolver/RemoveAdyenStateDataTest.php new file mode 100644 index 000000000..d4aa02201 --- /dev/null +++ b/Test/Unit/Model/Resolver/RemoveAdyenStateDataTest.php @@ -0,0 +1,175 @@ + + */ +namespace Adyen\Payment\Test\Model\Resolver; + +use Adyen\Payment\Exception\GraphQlAdyenException; +use Adyen\Payment\Model\Api\AdyenStateData; +use Adyen\Payment\Model\Resolver\RemoveAdyenStateData; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Exception; +use Magento\Catalog\Model\Layer\ContextInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class RemoveAdyenStateDataTest extends AbstractAdyenTestCase +{ + private $removeAdyenStateDataResolver; + private $adyenStateDataHelperMock; + private $quoteIdMaskFactoryMock; + private $quoteIdMaskMock; + private $fieldMock; + private $contextMock; + private $infoMock; + + public function setUp(): void + { + $this->objectManager = new ObjectManager($this); + + $this->adyenStateDataHelperMock = $this->createMock(AdyenStateData::class); + $this->fieldMock = $this->createMock(Field::class); + $this->contextMock = $this->createMock(ContextInterface::class); + $this->infoMock = $this->createMock(ResolveInfo::class); + + $this->quoteIdMaskMock = $this->createGeneratedMock(QuoteIdMask::class, ['load', 'getQuoteId']); + $this->quoteIdMaskMock->method('load')->willReturn($this->quoteIdMaskMock); + $this->quoteIdMaskMock->method('getQuoteId')->willReturn(1); + + $this->quoteIdMaskFactoryMock = $this->createGeneratedMock(QuoteIdMaskFactory::class, [ + 'create' + ]); + $this->quoteIdMaskFactoryMock->method('create')->willReturn($this->quoteIdMaskMock); + + $this->removeAdyenStateDataResolver = $this->objectManager->getObject(RemoveAdyenStateData::class, [ + 'adyenStateData' => $this->adyenStateDataHelperMock, + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock + ]); + } + + public function testResolve() + { + $stateDataId = 1; + + $args = [ + 'stateDataId' => $stateDataId, + 'cartId' => 1 + ]; + + $this->adyenStateDataHelperMock->expects($this->once())->method('remove')->willReturn(true); + + $result = $this->removeAdyenStateDataResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + + $this->assertArrayHasKey('stateDataId', $result); + $this->assertEquals($stateDataId, $result['stateDataId']); + } + + public function testResolveWithLocalizedException() + { + $this->expectException(LocalizedException::class); + + $stateDataId = 1; + + $args = [ + 'stateDataId' => $stateDataId, + 'cartId' => 1 + ]; + + $this->adyenStateDataHelperMock->expects($this->once())->method('remove')->willReturn(false); + + $result = $this->removeAdyenStateDataResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + public function testResolveWithGraphQLAdyenException() + { + $this->expectException(GraphQlAdyenException::class); + + $args = [ + 'stateDataId' => 1, + 'cartId' => 1 + ]; + + $this->adyenStateDataHelperMock->expects($this->once()) + ->method('remove') + ->willThrowException(new Exception()); + + $this->removeAdyenStateDataResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + /** + * @dataProvider inputFailureDataProvider + */ + public function testResolveFailureWithWrongInput($stateDataId, $cartId) + { + $this->expectException(GraphQlInputException::class); + + $args = [ + 'stateDataId' => $stateDataId, + 'cartId' => $cartId + ]; + + $this->removeAdyenStateDataResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + /** + * Data provider for testResolveFailureWithWrongInput test method + * + * @return array + */ + private static function inputFailureDataProvider(): array + { + return [ + [ + 'stateDataId' => '', + 'cartId' => 1 + ], + [ + 'stateDataId' => 1, + 'cartId' => '' + ], + [ + 'stateDataId' => '', + 'cartId' => '' + ], + [ + 'stateDataId' => null, + 'cartId' => 1 + ] + ]; + } +} diff --git a/Test/Unit/Model/Resolver/SaveAdyenStateDataTest.php b/Test/Unit/Model/Resolver/SaveAdyenStateDataTest.php new file mode 100644 index 000000000..871e291f5 --- /dev/null +++ b/Test/Unit/Model/Resolver/SaveAdyenStateDataTest.php @@ -0,0 +1,152 @@ + + */ +namespace Adyen\Payment\Test\Model\Resolver; + +use Adyen\Payment\Model\Api\AdyenStateData; +use Adyen\Payment\Model\Resolver\SaveAdyenStateData; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Exception; +use Magento\Catalog\Model\Layer\ContextInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class SaveAdyenStateDataTest extends AbstractAdyenTestCase +{ + private $saveAdyenStateDataResolver; + private $adyenStateDataHelperMock; + private $quoteIdMaskFactoryMock; + private $quoteIdMaskMock; + private $fieldMock; + private $contextMock; + private $infoMock; + + public function setUp(): void + { + $this->objectManager = new ObjectManager($this); + + $this->adyenStateDataHelperMock = $this->createMock(AdyenStateData::class); + $this->fieldMock = $this->createMock(Field::class); + $this->contextMock = $this->createMock(ContextInterface::class); + $this->infoMock = $this->createMock(ResolveInfo::class); + + $this->quoteIdMaskMock = $this->createGeneratedMock(QuoteIdMask::class, ['load', 'getQuoteId']); + $this->quoteIdMaskMock->method('load')->willReturn($this->quoteIdMaskMock); + $this->quoteIdMaskMock->method('getQuoteId')->willReturn(1); + + $this->quoteIdMaskFactoryMock = $this->createGeneratedMock(QuoteIdMaskFactory::class, [ + 'create' + ]); + $this->quoteIdMaskFactoryMock->method('create')->willReturn($this->quoteIdMaskMock); + + $this->saveAdyenStateDataResolver = $this->objectManager->getObject(SaveAdyenStateData::class, [ + 'adyenStateData' => $this->adyenStateDataHelperMock, + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock + ]); + } + + public function testResolve() + { + $stateData = "{\"paymentMethod\":{\"type\":\"giftcard\",\"brand\":\"svs\",\"encryptedCardNumber\":\"abd...\",\"encryptedSecurityCode\":\"xyz...\"},\"giftcard\":{\"balance\":{\"currency\":\"EUR\",\"value\":5000},\"title\":\"SVS\"}}"; + $stateDataId = 1; + + $args = [ + 'stateData' => $stateData, + 'cartId' => 1 + ]; + + $this->adyenStateDataHelperMock->expects($this->once())->method('save')->willReturn($stateDataId); + + $result = $this->saveAdyenStateDataResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + + $this->assertArrayHasKey('stateDataId', $result); + $this->assertEquals($stateDataId, $result['stateDataId']); + } + + public function testResolveFailedWithException() + { + $this->expectException(Exception::class); + + $args = [ + 'stateData' => "{}", + 'cartId' => 1 + ]; + + $this->adyenStateDataHelperMock->expects($this->once()) + ->method('save') + ->willThrowException(new Exception()); + + $result = $this->saveAdyenStateDataResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + /** + * @dataProvider inputFailureDataProvider + */ + public function testResolveFailureWithWrongInput($stateData, $cartId) + { + $this->expectException(GraphQlInputException::class); + + $args = [ + 'stateData' => $stateData, + 'cartId' => $cartId + ]; + + $this->saveAdyenStateDataResolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + null, + $args + ); + } + + /** + * Data provider for testResolveFailureWithWrongInput test method + * + * @return array + */ + private static function inputFailureDataProvider(): array + { + return [ + [ + 'stateData' => '', + 'cartId' => 1 + ], + [ + 'stateData' => "{}", + 'cartId' => '' + ], + [ + 'stateData' => '', + 'cartId' => '' + ], + [ + 'stateData' => null, + 'cartId' => 1 + ] + ]; + } +} diff --git a/Test/Unit/Setup/RecurringDataTest.php b/Test/Unit/Setup/RecurringDataTest.php index 142f21b58..51f9a45de 100644 --- a/Test/Unit/Setup/RecurringDataTest.php +++ b/Test/Unit/Setup/RecurringDataTest.php @@ -18,7 +18,7 @@ use Adyen\Payment\Helper\PaymentMethodsFactory; use Adyen\Payment\Helper\PaymentMethods; -class RecurringTest extends AbstractAdyenTestCase +class RecurringDataTest extends AbstractAdyenTestCase { private RecurringData $recurringData; diff --git a/etc/schema.graphqls b/etc/schema.graphqls index 0a395bc0c..21cc07b11 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -9,6 +9,14 @@ type Query { shopper_locale: String @doc(description: "Language and country code combination separated by underscore (_), e.g. en_US") country: String @doc(description: "Country code to be used in paymentMethods call, e.g. US") ) : AdyenPaymentMethods @resolver(class: "Adyen\\Payment\\Model\\Resolver\\GetAdyenPaymentMethods") + + adyenPaymentMethodsBalance ( + payload: String! @doc(description: "JSON encoded payload with giftcard state data and amount.") + ): AdyenPaymentMethodsBalanceResponse @resolver(class: "Adyen\\Payment\\Model\\Resolver\\GetAdyenPaymentMethodsBalance") + + adyenRedeemedGiftcards ( + cartId: String! @doc(description: "Cart ID for which to fetch redeemed gift cards.") + ): AdyenRedeemedGiftcardsResponse @resolver(class: "Adyen\\Payment\\Model\\Resolver\\GetAdyenRedeemedGiftcards") } type Mutation { @@ -16,6 +24,16 @@ type Mutation { payload: String! @doc(description: "Payload JSON String with orderId, details, paymentData and threeDSAuthenticationOnly.") cart_id: String! @doc(description: "Cart ID.") ) : AdyenPaymentStatus @resolver(class: "Adyen\\Payment\\Model\\Resolver\\GetAdyenPaymentDetails") + + adyenSaveStateData ( + stateData: String! @doc(description: "JSON string of Adyen state data.") + cartId: String! @doc(description: "Cart ID associated with the state data.") + ): AdyenStateData @resolver(class: "Adyen\\Payment\\Model\\Resolver\\SaveAdyenStateData") + + adyenRemoveStateData( + stateDataId: Int! @doc(description: "ID of the state data to remove.") + cartId: String! @doc(description: "Cart ID associated with the state data.") + ): AdyenStateData @resolver(class: "Adyen\\Payment\\Model\\Resolver\\RemoveAdyenStateData") } type AdyenPaymentStatus { @@ -90,6 +108,33 @@ type AdyenPaymentMethodsDetailsItems { name: String @doc(description: "The display name.") } +type AdyenPaymentMethodsBalanceResponse { + balanceResponse: String @doc(description: "Balance of the payment method.") +} + +type AdyenRedeemedGiftcardsResponse { + redeemedGiftcards: [AdyenGiftcard] @doc(description: "List of redeemed gift cards.") + remainingAmount: String @doc(description: "Remaining order amount after giftcard discount.") + totalDiscount: String @doc(description: "Total giftcard discount applied to the cart.") +} + +type PaymentMethodBalance { + currency: String @doc(description: "Cart currency") + value: String @doc(description: "Total cart amount") +} + +type AdyenGiftcard { + stateDataId: String @doc(description: "Gift card identifier.") + brand: String @doc(description: "Gift card brand") + title: String @doc(description: "Gift card payment method title") + balance: PaymentMethodBalance @doc(description: "Remaining balance on the gift card.") + deductedAmount: String @doc(description: "Deducted balance from the gift card") +} + +type AdyenStateData { + stateDataId: String @doc(description: "ID of the inserted stateData object.") +} + type AdyenPaymentMethodsExtraDetails { type: String @doc(description: "The unique payment method code.") icon: AdyenPaymentMethodIcon @doc(description: "Icon for the payment method.")