From 776913d4407a4279be5281122050c90525ceecd0 Mon Sep 17 00:00:00 2001 From: bortefi <44976351+bortefi@users.noreply.github.com> Date: Wed, 30 Mar 2022 19:16:34 +0200 Subject: [PATCH] Release 3.5.0 - USP (#206) * ASW-428 fix unique name * ASW-428 add final keyaword * ASW-426 restructure backend subscribers * ASW-426 refactor providers * ASW-426 add legacy type converter to unique identifier * ASW-426 final * ASW-381 cleanup * ASW-381 clearify (wip) * ASW-381 plugin id provider * ASW-381 cleanup services, fix googleplay Signed-off-by: Filippe Bortels * ASW-381 fix issue with translated paymentmethod names, change unique identifier to code Signed-off-by: Filippe Bortels * ASW-393 add brand in minimal state so giftcards are supported * ASW-393 refactor giftcards out of minimal state, build it using checkout component data * ASW-393 refactor isGiftCard * ASW-393 add brand in minimal state so giftcards are supported # Conflicts: # Resources/views/frontend/checkout/adyen_configuration.tpl * ASW-393 refactor isGiftCard * ASW-381 clearify (wip) * ASW-392 - Replaced subscriber by frontend check * ASW-392 - CR fix for URL * ASW-392 - CR fix for JSON * ASW-392 - CR fix for URL * ASW-392 refactor let to var, remove optional chaining * ASW-392 rewrite cookie allowed * ASW-392 fix allow cookies * ASW-392 fix rebase issues * ASW-392 remove comments next to cookies allowed * ASW-393 WIP: check session type * ASW-393 add fixes for case init and change payment * ASW-393 fix minimal state from backend on gift cards * ASW-393 fix grumphp blacklist dd regex * ASW-0 update grump config * ASW-0 3.2.0 changelog * ASW-0 update composer * ASW-429 add guzzlehttp * ASW-429 WIP add http client apple pay * ASW-429: WIP add zip extractor and fallback logic * ASW-429 add apple pay certificate decoder unit test * ASW-429 add apple pay certificate encoder unit test * ASW-429 add http code to log level unit test * ASW-429 add response status to log level unit test * ASW-429 add body logging middleware unit test * ASW-429 add header logging middleware unit test * ASW-429 add apple pay unit test * ASW-429 add apple pay handler unit test * ASW-429 add apple pay request unit test * ASW-429 add apple pay response unit test * ASW-429 add certificate writer unit test * ASW-429 WIP add zip extractor unit test * ASW-429 add stream transport factory unit test * ASW-429 remove to/file * ASW-429 add assertions on file removal on CertificateWriterTest * ASW-429 fix ZipExtractorTest * ASW-429 refactor apple pay certificate code review * ASW-429 remove suppress from HeaderLoggingMiddleware * ASW-429 rename service xml * ASW-429 remove body from logging * ASW-429 make PlainTextEncoder final * ASW-429 add original exception to CouldNotWriteCertificate * ASW-429 make CertificateWriter more specific, update tests * ASW-429 refactor ZipExtractorTest * ASW-429 re-add CertificateWriterTest * ASW-429 keep exception history * ASW-429 refactor logging infi * ASW-429 rename ApplePay model to ApplePayCertificate * ASW-429 add parameter base_uri * ASW-429 rename ApplePayRequest to ApplePayCertificateRequest, change tests * ASW-429 refactor handler, decoder and make the shit work * ASW-429 remove comment * ASW-429 rename ApplePayTestCertificate to postfix Test * ASW-429: refactor the shit out of it * ASW-429 change const usage * ASW-429 remove unused dependency * ASW-429 inject stream transport as a factory * ASW-430 WIP add apple pay certificate url rewriter * ASW-430 WIP working url rewrite for cronjob * ASW-430 make certificate available on endpoint * ASW-430 make apple pay certificate available on endpoint using cronjob and live * ASW-430 WIP add ApplePayTransportHandler on install * ASW-430: WIP add import button and functionality * ASW-429 ASW-430 rework Adyen ApplePay Merchant id assocation * ASW-429-430 cleanup config * ASW-429-430 remove obsolete config * ASW-429-430 keep zip fixture * ASW-429 ASW-430 fix type condition * ASW-429 ASW-430 fix type condition, with error code * ASW-429 ASW-430 update composer lock * ASW-0 bump version 3.3.0, add plugin notes * ASW-401: fix adyen plugin compatible with SWAGPayPalUnified * ASW-401 add escapeWithQuotes to Sanitize * ASW-401 add unit test SanitizeTest * ASW-401 add escapeWithQuotes testcase to SwPaymentMeanSerializerTest * ASW-401 remove Object.values on ApplePayMethod and getPaymentMethodId * ASW-401 remove object values on setConfig adyen enriched payment methods * ASW-401 WIP refactor sAdyenConfig using endpoint instead of data attribute * ASW-401 remove data- attribute for sAdyenConfig * ASW-401 refactor urls * ASW-432 handle apple pay component request * ASW-432 make currency dynamic * ASW-432 WIP make adyenOrderTotal available over config api endpoint * ASW-432 fix adyenOrderTotal and currency to be fetched using ajax via config * ASW-432 refactor handle to buildComponentApplePay * ASW-432 refactor ApplePay build component data to checkout component data * ASW-432 remove session from buildcomponentdata and move it to handleComponent * ASW-432 add comment check early return is possible in future * ASW-432 remove todo comment after testing it * ASW-0 update php-cs-fixer-config * ASW-0 update release notes * ASW-0 update pipline for master branch * ASW-436 fix nullable data from attribute, add enriched test * ASW-436 handle exceptions * ASW-436 clear config cache on backen api action * ASW-376 - Stored payment methods * ASW-376 - Unit test * ASW-375 - Stored payment umbrella installation * ASW-375 - Query * ASW-375 - Hide from admin * ASW-375 - Unit tests * ASW-375 - CR fixes * ASW-375 - Hide field check * ASW-375 - Hide on backend shipping section * ASW-375 - Secure susbscribers * ASW-375 - ASW-379 - Hide refactor * ASW-375 - CR fixes * ASW-375 - CR fixes * ASW-375 refactor code * ASW-375 rename test case * ASW-375 cs flavour * ASW-375 cleanup code, fix test: use real implementation over mocks * ASW-376 - Unit tests * ASW-376 - CR fixes * ASW-378 - Disabled stored methods pick * ASW-378 - Template fix * ASW-377 - EnrichedPaymentMeanProvider for stored methods * ASW-377 - Stored methods selection * ASW-377 - Payment mean selection * ASW-377 - Subscribers and session handler * ASW-377 - CR fixes * ASW-377 - Stored Method ID save and CR fixes * ASW-377 - PaymentMeanCollection tests * ASW-377 - EnrichedPaymentMeanProviderTest updated * ASW-377 - PaymentMethodEnricher unit test * ASW-377 - Admin get payments refactor and unit tests * ASW-377 - Fixed issue and unit tests * ASW-377 - PersistStoredMethodIdSubscriber unit tests * ASW-377 - Fixed config * ASW-435 - Attribute removed * ASW-377 - CR fixes * ASW-377 - CR fixes * ASW-377 fix add subshops on install/update * ASW-461 - My account preselected stored methods * ASW-461 - Preselected stored methods in checkout * ASW-461 - Fixed unit tests * ASW-461 - New unit test * ASW-461 - Account subscriber unit test * ASW-461 - Account subscriber unit test fix * ASW-461 - Renamed test * ASW-461 - Test clarification * ASW-461 - Unused class * ASW-461 - User preference unit test * ASW-461 - CR fixes * ASW-461 - CR fixes * ASW-461 - CR fixes * ASW-461 - CR fix * ASW-461 - Single fetch fix * ASW-466 - Guests fix * ASW-466 - Fix type * ASW-466 - Test name fix * ASW-466 - CR fix * ASW-0 Bump version 3.3.1 * ASW-0 Bump version 3.4.0, changelog * Release 3.4.0 (#203) * ASW-0 update pipline for master branch * ASW-436 fix nullable data from attribute, add enriched test * ASW-436 handle exceptions * ASW-436 clear config cache on backen api action * ASW-376 - Stored payment methods * ASW-376 - Unit test * ASW-375 - Stored payment umbrella installation * ASW-375 - Query * ASW-375 - Hide from admin * ASW-375 - Unit tests * ASW-375 - CR fixes * ASW-375 - Hide field check * ASW-375 - Hide on backend shipping section * ASW-375 - Secure susbscribers * ASW-375 - ASW-379 - Hide refactor * ASW-375 - CR fixes * ASW-375 - CR fixes * ASW-375 refactor code * ASW-375 rename test case * ASW-375 cs flavour * ASW-375 cleanup code, fix test: use real implementation over mocks * ASW-376 - Unit tests * ASW-376 - CR fixes * ASW-378 - Disabled stored methods pick * ASW-378 - Template fix * ASW-377 - EnrichedPaymentMeanProvider for stored methods * ASW-377 - Stored methods selection * ASW-377 - Payment mean selection * ASW-377 - Subscribers and session handler * ASW-377 - CR fixes * ASW-377 - Stored Method ID save and CR fixes * ASW-377 - PaymentMeanCollection tests * ASW-377 - EnrichedPaymentMeanProviderTest updated * ASW-377 - PaymentMethodEnricher unit test * ASW-377 - Admin get payments refactor and unit tests * ASW-377 - Fixed issue and unit tests * ASW-377 - PersistStoredMethodIdSubscriber unit tests * ASW-377 - Fixed config * ASW-435 - Attribute removed * ASW-377 - CR fixes * ASW-377 - CR fixes * ASW-377 fix add subshops on install/update * ASW-461 - My account preselected stored methods * ASW-461 - Preselected stored methods in checkout * ASW-461 - Fixed unit tests * ASW-461 - New unit test * ASW-461 - Account subscriber unit test * ASW-461 - Account subscriber unit test fix * ASW-461 - Renamed test * ASW-461 - Test clarification * ASW-461 - Unused class * ASW-461 - User preference unit test * ASW-461 - CR fixes * ASW-461 - CR fixes * ASW-461 - CR fixes * ASW-461 - CR fix * ASW-461 - Single fetch fix * ASW-466 - Guests fix * ASW-466 - Fix type * ASW-466 - Test name fix * ASW-466 - CR fix * ASW-0 Bump version 3.3.1 * ASW-0 Bump version 3.4.0, changelog Co-authored-by: Damian Pastorini Co-authored-by: davebueds * ASW-0 bump version 3.5.0 * ASW-476 refactor result codes, add result code handler, add tests, refactor * ASW-472 add model ShopperInteraction and test * ASW-476 add missing unit test cases * ASW-473 add model RecurringProcessingModel and test * ASW-477 add CustomerNumberProvider, test and refactor PaymentMethodService * ASW-470 add RecurringPaymentToken model and unit test * ASW-472 Add shopperinteractions, fix tests * ASW-473 fix cr feedback * ASW-473 fix cr feedback * ASW-477 apply fixes CR * ASW-470 make orderNumber not nullable, add tests, add indexes * ASW-476 add invalid status + test * ASW-469 Add mapper RecurringTokenMapper and unit test * ASW-469 add invalidPaymentsResponseException + test * ASW-469 refactor test * ASW-476 remove PaymentResultCodeHandler and test, leave it in Adyen.php * ASW-476 add extra test cases * ASW-470 refactor so it uses uuid tokens * ASW-470 id strategy none * ASW-471 WIP add RecurringPaymentTokenRepository * ASW-471 add config and traceable recurring payment token repository * ASW-473 - CR fixes * ASW-473 - CR fix * ASW-476 - CR fixes * ASW-476 - CR fixes * ASW-469 - CR fixes * ASW-471 - CR fixes and unit tests * ASW-471 - CR fixes and tests * ASW-471 - CR fixes * ASW-477 - CR comment * ASW-470 - CR fixes * ASW-470 - CR fixes * ASW-474 - Recurring one off payment token provider * ASW-474 - Validation changes * ASW-474 - Changes and unit tests * ASW-476 - CR fixes * ASW-471 - CR fixes * ASW-469 - CR fixes * ASW-472 - CR fixes * ASW-479 Recurring-disable * ASW-479 update cs * ASW-499 - Disable token request handler * ASW-499 - Disable token request handler interface * ASW-499 - Refactor and unit tests * ASW-499 - Unit tests * ASW-499 - DisableTokenRequestHandler unit tests * ASW-499 - Fix * ASW-499 - Handler changes * ASW-499 - FrontendJsonResponse test * ASW-499 - Integration test file * ASW-499 - CR fixes * ASW-499 - Logger assert * ASW-499 - Validate message * ASW-499 - CR change * ASW-500 - Frontend controller for disable token * ASW-500 - Result changed * ASW-500 - Method name * ASW-500 - Controller name * ASW-500 - Controller folder * ASW-500 - Config fixes * ASW-500 - Unit test note * ASW-500 - CR fix * ASW-500 - Refactor after rebase * ASW-500 - CR fixes * ASW-500 - CR fixes * ASW-500 - CR fixes * ASW-501 - Disable payment button * ASW-501 - ASW-500 related changes * ASW-501 - CR changes * ASW-501 - API URL * ASW-501 fix data attributes (url broken) * ASW-0 update changelog Co-authored-by: davebueds Co-authored-by: Damian Pastorini --- .../HttpClient/ClientFactory.php | 34 ++-- .../HttpClient/ClientFactoryInterface.php | 13 ++ .../HttpClient/ClientMemoise.php | 12 +- .../HttpClient/ClientMemoiseInterface.php | 13 ++ .../HttpClient/ConfigValidator.php | 29 +-- .../HttpClient/ConfigValidatorInterface.php | 12 ++ AdyenApi/Model/ApiResponse.php | 37 ++++ .../Recurring/DisableTokenRequestHandler.php | 55 +++++ .../DisableTokenRequestHandlerInterface.php | 13 ++ AdyenApi/TransportFactory.php | 34 ++++ AdyenApi/TransportFactoryInterface.php | 15 ++ AdyenPayment.php | 2 + .../PaymentMethod/PaymentMethodsProvider.php | 6 +- Components/Adyen/PaymentMethodService.php | 40 +--- .../Adyen/PaymentMethodServiceInterface.php | 3 +- Components/Adyen/RefundService.php | 27 +-- Components/Configuration.php | 2 +- Components/ConfigurationInterface.php | 25 +++ .../RecurringOneOffPaymentTokenProvider.php | 27 +++ .../Providers/RecurringPaymentProvider.php | 12 +- Controllers/Backend/TestAdyenApi.php | 8 +- Controllers/Frontend/Adyen.php | 59 ++---- .../Frontend/DisableRecurringToken.php | 72 +++++++ Controllers/Frontend/Process.php | 44 ++-- .../InvalidPaymentsResponseException.php | 13 ++ ...RecurringPaymentTokenNotFoundException.php | 28 +++ ...RecurringPaymentTokenNotSavedException.php | 15 ++ Http/Response/ApiJsonResponse.php | 24 +++ Http/Response/FrontendJsonResponse.php | 42 ++++ Models/Enum/PaymentResultCodes.php | 18 -- Models/PaymentResultCode.php | 118 +++++++++++ .../RecurringPaymentToken.php | 191 ++++++++++++++++++ .../RecurringProcessingModel.php | 56 +++++ .../RecurringPayment/ShopperInteraction.php | 58 ++++++ Models/TokenIdentifier.php | 38 ++++ Recurring/RecurringTokenFactory.php | 33 +++ Recurring/RecurringTokenFactoryInterface.php | 12 ++ .../RecurringPaymentTokenRepository.php | 57 ++++++ ...curringPaymentTokenRepositoryInterface.php | 14 ++ ...aceableRecurringPaymentTokenRepository.php | 59 ++++++ .../js/jquery.adyen-disable-payment.js | 64 ++++++ Resources/frontend/js/jquery.plugin-loader.js | 3 +- Resources/services/adyen-api.xml | 20 ++ Resources/services/components.xml | 25 +-- Resources/services/http.xml | 2 + Resources/services/payload-providers.xml | 56 +++++ Resources/services/providers.xml | 2 +- Resources/services/repositories.xml | 19 +- Resources/services/session.xml | 4 + Resources/services/validators.xml | 15 +- .../frontend/register/payment_fieldset.tpl | 18 ++ Session/CustomerNumberProvider.php | 32 +++ Session/CustomerNumberProviderInterface.php | 10 + plugin.xml | 14 +- .../DisableTokenRequestHandlerTest.php | 15 ++ .../AdyenApi/HttpClient/ClientFactoryTest.php | 83 ++++++++ .../AdyenApi/HttpClient/ClientMemoiseTest.php | 61 ++++++ .../HttpClient/ConfigValidatorTest.php | 157 ++++++++++++++ .../DisableTokenRequestHandlerTest.php | 103 ++++++++++ tests/Unit/AdyenApi/TransportFactoryTest.php | 84 ++++++++ ...ecurringOneOffPaymentTokenProviderTest.php | 58 ++++++ .../RecurringPaymentProviderTest.php | 58 ++++++ ...rringPaymentTokenNotFoundExceptionTest.php | 55 +++++ ...rringPaymentTokenNotSavedExceptionTest.php | 39 ++++ .../Response/FrontendJsonResponseTest.php | 55 +++++ tests/Unit/Models/PaymentResultCodeTest.php | 90 +++++++++ .../RecurringPaymentTokenTest.php | 144 +++++++++++++ .../RecurringProcessingModelTest.php | 73 +++++++ .../ShopperInteractionTest.php | 70 +++++++ tests/Unit/Models/TokenIdentifierTest.php | 55 +++++ .../Recurring/RecurringTokenFactoryTest.php | 89 ++++++++ .../RecurringPaymentTokenRepositoryTest.php | 111 ++++++++++ ...bleRecurringPaymentTokenRepositoryTest.php | 152 ++++++++++++++ .../Session/CustomerNumberProviderTest.php | 87 ++++++++ 74 files changed, 3072 insertions(+), 221 deletions(-) rename Components/Adyen/ApiFactory.php => AdyenApi/HttpClient/ClientFactory.php (66%) create mode 100644 AdyenApi/HttpClient/ClientFactoryInterface.php rename Components/Adyen/ApiClientMap.php => AdyenApi/HttpClient/ClientMemoise.php (70%) create mode 100644 AdyenApi/HttpClient/ClientMemoiseInterface.php rename Components/Adyen/ApiConfigValidator.php => AdyenApi/HttpClient/ConfigValidator.php (83%) create mode 100644 AdyenApi/HttpClient/ConfigValidatorInterface.php create mode 100644 AdyenApi/Model/ApiResponse.php create mode 100644 AdyenApi/Recurring/DisableTokenRequestHandler.php create mode 100644 AdyenApi/Recurring/DisableTokenRequestHandlerInterface.php create mode 100644 AdyenApi/TransportFactory.php create mode 100644 AdyenApi/TransportFactoryInterface.php create mode 100644 Components/ConfigurationInterface.php create mode 100644 Components/Payload/Providers/RecurringOneOffPaymentTokenProvider.php create mode 100644 Controllers/Frontend/DisableRecurringToken.php create mode 100644 Exceptions/InvalidPaymentsResponseException.php create mode 100644 Exceptions/RecurringPaymentTokenNotFoundException.php create mode 100644 Exceptions/RecurringPaymentTokenNotSavedException.php create mode 100644 Http/Response/ApiJsonResponse.php create mode 100644 Http/Response/FrontendJsonResponse.php delete mode 100644 Models/Enum/PaymentResultCodes.php create mode 100644 Models/PaymentResultCode.php create mode 100644 Models/RecurringPayment/RecurringPaymentToken.php create mode 100644 Models/RecurringPayment/RecurringProcessingModel.php create mode 100644 Models/RecurringPayment/ShopperInteraction.php create mode 100644 Models/TokenIdentifier.php create mode 100644 Recurring/RecurringTokenFactory.php create mode 100644 Recurring/RecurringTokenFactoryInterface.php create mode 100644 Repository/RecurringPayment/RecurringPaymentTokenRepository.php create mode 100644 Repository/RecurringPayment/RecurringPaymentTokenRepositoryInterface.php create mode 100644 Repository/RecurringPayment/TraceableRecurringPaymentTokenRepository.php create mode 100644 Resources/frontend/js/jquery.adyen-disable-payment.js create mode 100644 Resources/services/adyen-api.xml create mode 100644 Resources/services/payload-providers.xml create mode 100644 Session/CustomerNumberProvider.php create mode 100644 Session/CustomerNumberProviderInterface.php create mode 100644 tests/Integration/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php create mode 100644 tests/Unit/AdyenApi/HttpClient/ClientFactoryTest.php create mode 100644 tests/Unit/AdyenApi/HttpClient/ClientMemoiseTest.php create mode 100644 tests/Unit/AdyenApi/HttpClient/ConfigValidatorTest.php create mode 100644 tests/Unit/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php create mode 100644 tests/Unit/AdyenApi/TransportFactoryTest.php create mode 100644 tests/Unit/Components/Payload/Providers/RecurringOneOffPaymentTokenProviderTest.php create mode 100644 tests/Unit/Components/Payload/Providers/RecurringPaymentProviderTest.php create mode 100644 tests/Unit/Exceptions/RecurringPaymentTokenNotFoundExceptionTest.php create mode 100644 tests/Unit/Exceptions/RecurringPaymentTokenNotSavedExceptionTest.php create mode 100644 tests/Unit/Http/Response/FrontendJsonResponseTest.php create mode 100644 tests/Unit/Models/PaymentResultCodeTest.php create mode 100644 tests/Unit/Models/RecurringPayment/RecurringPaymentTokenTest.php create mode 100644 tests/Unit/Models/RecurringPayment/RecurringProcessingModelTest.php create mode 100644 tests/Unit/Models/RecurringPayment/ShopperInteractionTest.php create mode 100644 tests/Unit/Models/TokenIdentifierTest.php create mode 100644 tests/Unit/Recurring/RecurringTokenFactoryTest.php create mode 100644 tests/Unit/Repository/RecurringPayment/RecurringPaymentTokenRepositoryTest.php create mode 100644 tests/Unit/Repository/RecurringPayment/TraceableRecurringPaymentTokenRepositoryTest.php create mode 100644 tests/Unit/Session/CustomerNumberProviderTest.php diff --git a/Components/Adyen/ApiFactory.php b/AdyenApi/HttpClient/ClientFactory.php similarity index 66% rename from Components/Adyen/ApiFactory.php rename to AdyenApi/HttpClient/ClientFactory.php index f22047e4..4ce3a94d 100644 --- a/Components/Adyen/ApiFactory.php +++ b/AdyenApi/HttpClient/ClientFactory.php @@ -2,34 +2,22 @@ declare(strict_types=1); -namespace AdyenPayment\Components\Adyen; +namespace AdyenPayment\AdyenApi\HttpClient; use Adyen\AdyenException; use Adyen\Client; use Adyen\Environment; -use AdyenPayment\Components\Configuration; +use AdyenPayment\Components\ConfigurationInterface; use Psr\Log\LoggerInterface; use Shopware\Models\Shop\Shop; -/** - * Class ApiFactory. - */ -class ApiFactory +final class ClientFactory implements ClientFactoryInterface { - /** - * @var Configuration - */ - private $configuration; + private ConfigurationInterface $configuration; + private LoggerInterface $logger; - /** - * @var LoggerInterface - */ - private $logger; - - public function __construct( - Configuration $configuration, - LoggerInterface $logger - ) { + public function __construct(ConfigurationInterface $configuration, LoggerInterface $logger) + { $this->configuration = $configuration; $this->logger = $logger; } @@ -47,8 +35,12 @@ public function provide(Shop $shop): Client ); } - private function createClient(string $merchantAccount, string $apiKey, string $environment, ?string $prefix = null): Client - { + private function createClient( + string $merchantAccount, + string $apiKey, + string $environment, + ?string $prefix = null + ): Client { $urlPrefix = Environment::LIVE === $environment ? $prefix : null; $adyenClient = new Client(); diff --git a/AdyenApi/HttpClient/ClientFactoryInterface.php b/AdyenApi/HttpClient/ClientFactoryInterface.php new file mode 100644 index 00000000..6cd6fb44 --- /dev/null +++ b/AdyenApi/HttpClient/ClientFactoryInterface.php @@ -0,0 +1,13 @@ + */ private $memoisedClients = []; + private ClientFactoryInterface $factory; - /** - * @var ApiFactory - */ - private $factory; - - public function __construct(ApiFactory $factory) + public function __construct(ClientFactoryInterface $factory) { $this->factory = $factory; } diff --git a/AdyenApi/HttpClient/ClientMemoiseInterface.php b/AdyenApi/HttpClient/ClientMemoiseInterface.php new file mode 100644 index 00000000..da6263eb --- /dev/null +++ b/AdyenApi/HttpClient/ClientMemoiseInterface.php @@ -0,0 +1,13 @@ +adyenApiFactory = $adyenApiFactory; @@ -42,7 +31,7 @@ public function __construct( public function validate(int $shopId): ConstraintViolationList { $shop = $this->shopRepository->find($shopId); - if (!$shop) { + if (null === $shop) { return new ConstraintViolationList([ ConstraintViolationFactory::create('Shop not found for ID "'.$shopId.'".'), ]); diff --git a/AdyenApi/HttpClient/ConfigValidatorInterface.php b/AdyenApi/HttpClient/ConfigValidatorInterface.php new file mode 100644 index 00000000..610542b9 --- /dev/null +++ b/AdyenApi/HttpClient/ConfigValidatorInterface.php @@ -0,0 +1,12 @@ +success = $success; + $this->message = $message; + } + + public static function create(bool $success, string $message): self + { + return new self($success, $message); + } + + public static function empty(): self + { + return new self(false, 'Customer number not found.'); + } + + public function isSuccess(): bool + { + return $this->success; + } + + public function message(): string + { + return $this->message; + } +} diff --git a/AdyenApi/Recurring/DisableTokenRequestHandler.php b/AdyenApi/Recurring/DisableTokenRequestHandler.php new file mode 100644 index 00000000..bc39da6a --- /dev/null +++ b/AdyenApi/Recurring/DisableTokenRequestHandler.php @@ -0,0 +1,55 @@ +transportFactory = $transportFactory; + $this->customerNumberProvider = $customerNumberProvider; + } + + public function disableToken(string $recurringTokenId, Shop $shop): ApiResponse + { + $customerNumber = ($this->customerNumberProvider)(); + if ('' === $customerNumber) { + return ApiResponse::empty(); + } + $recurringTransport = $this->transportFactory->recurring($shop); + + $payload = [ + 'shopperReference' => $customerNumber, + 'recurringDetailReference' => $recurringTokenId, + ]; + + $result = $recurringTransport->disable($payload); + + $response = (string) ($result['response'] ?? ''); + $resultMessage = (string) ($result['message'] ?? ''); + $isSuccessfullyDisabled = $this->isSuccessfullyDisabled($response); + + return ApiResponse::create($isSuccessfullyDisabled, $resultMessage); + } + + private function isSuccessfullyDisabled(string $response): bool + { + if (false === mb_strpos($response, 'successfully-disabled')) { + return false; + } + + return true; + } +} diff --git a/AdyenApi/Recurring/DisableTokenRequestHandlerInterface.php b/AdyenApi/Recurring/DisableTokenRequestHandlerInterface.php new file mode 100644 index 00000000..27317f1e --- /dev/null +++ b/AdyenApi/Recurring/DisableTokenRequestHandlerInterface.php @@ -0,0 +1,13 @@ +apiFactory = $apiFactory; + } + + public function recurring(Shop $shop): Recurring + { + return new Recurring( + $this->apiFactory->provide($shop) + ); + } + + public function checkout(Shop $shop): Checkout + { + return new Checkout( + $this->apiFactory->provide($shop) + ); + } +} diff --git a/AdyenApi/TransportFactoryInterface.php b/AdyenApi/TransportFactoryInterface.php new file mode 100644 index 00000000..e79d876c --- /dev/null +++ b/AdyenApi/TransportFactoryInterface.php @@ -0,0 +1,15 @@ +getClassMetadata(Refund::class), $entityManager->getClassMetadata(TextNotification::class), $entityManager->getClassMetadata(UserPreference::class), + $entityManager->getClassMetadata(RecurringPaymentToken::class), ]; } diff --git a/Components/Adyen/PaymentMethod/PaymentMethodsProvider.php b/Components/Adyen/PaymentMethod/PaymentMethodsProvider.php index 76e27a29..f5e9defb 100644 --- a/Components/Adyen/PaymentMethod/PaymentMethodsProvider.php +++ b/Components/Adyen/PaymentMethod/PaymentMethodsProvider.php @@ -6,8 +6,8 @@ use Adyen\AdyenException; use Adyen\Service\Checkout; +use AdyenPayment\AdyenApi\HttpClient\ClientFactory; use AdyenPayment\Collection\Payment\PaymentMethodCollection; -use AdyenPayment\Components\Adyen\ApiFactory; use AdyenPayment\Components\Adyen\PaymentMethodService; use AdyenPayment\Components\Configuration; use Psr\Log\LoggerInterface; @@ -16,12 +16,12 @@ final class PaymentMethodsProvider implements PaymentMethodsProviderInterface { private Configuration $configuration; - private ApiFactory $adyenApiFactory; + private ClientFactory $adyenApiFactory; private LoggerInterface $logger; public function __construct( Configuration $configuration, - ApiFactory $adyenApiFactory, + ClientFactory $adyenApiFactory, LoggerInterface $logger ) { $this->configuration = $configuration; diff --git a/Components/Adyen/PaymentMethodService.php b/Components/Adyen/PaymentMethodService.php index 0200b269..11fd55de 100644 --- a/Components/Adyen/PaymentMethodService.php +++ b/Components/Adyen/PaymentMethodService.php @@ -7,42 +7,36 @@ use Adyen\AdyenException; use Adyen\Service\Checkout; use Adyen\Util\Currency; +use AdyenPayment\AdyenApi\HttpClient\ClientMemoise; use AdyenPayment\Collection\Payment\PaymentMethodCollection; use AdyenPayment\Components\Configuration; use AdyenPayment\Models\Enum\Channel; -use Enlight_Components_Session_Namespace; +use AdyenPayment\Session\CustomerNumberProviderInterface; use Psr\Log\LoggerInterface; -use Shopware\Components\Model\ModelManager; -use Shopware\Models\Customer\Customer; + +/** @TODO - Cleanup the public const (unify the services) and create unit tests */ final class PaymentMethodService implements PaymentMethodServiceInterface { - /** @todo cleanup the public const (unify the services) */ public const IMPORT_LOCALE = 'en_GB'; - private ApiClientMap $apiClientMap; + private ClientMemoise $apiClientMap; private Configuration $configuration; private array $cache; private LoggerInterface $logger; - private Enlight_Components_Session_Namespace $session; - private ModelManager $modelManager; + private CustomerNumberProviderInterface $customerNumberProvider; public function __construct( - ApiClientMap $apiClientMap, + ClientMemoise $apiClientMap, Configuration $configuration, LoggerInterface $logger, - Enlight_Components_Session_Namespace $session, - ModelManager $modelManager + CustomerNumberProviderInterface $customerNumberProvider ) { $this->apiClientMap = $apiClientMap; $this->configuration = $configuration; $this->logger = $logger; - $this->session = $session; - $this->modelManager = $modelManager; + $this->customerNumberProvider = $customerNumberProvider; } - /** - * @throws AdyenException - */ public function getPaymentMethods( ?string $countryCode = null, ?string $currency = null, @@ -70,7 +64,7 @@ public function getPaymentMethods( ], 'channel' => Channel::WEB, 'shopperLocale' => $locale, - 'shopperReference' => $this->provideCustomerNumber(), + 'shopperReference' => ($this->customerNumberProvider)(), ]; try { @@ -110,9 +104,6 @@ private function getCacheKey(string ...$keys): string return md5(implode(',', $keys)); } - /** - * @throws AdyenException - */ public function getCheckout(): Checkout { return new Checkout( @@ -121,15 +112,4 @@ public function getCheckout(): Checkout ) ); } - - private function provideCustomerNumber(): string - { - $userId = $this->session->get('sUserId'); - if (!$userId) { - return ''; - } - $customer = $this->modelManager->getRepository(Customer::class)->find($userId); - - return $customer ? (string) $customer->getNumber() : ''; - } } diff --git a/Components/Adyen/PaymentMethodServiceInterface.php b/Components/Adyen/PaymentMethodServiceInterface.php index ace594de..d7344319 100644 --- a/Components/Adyen/PaymentMethodServiceInterface.php +++ b/Components/Adyen/PaymentMethodServiceInterface.php @@ -18,7 +18,8 @@ public function getPaymentMethods( ): PaymentMethodCollection; /** - * @internal + * @deprecated + * @see \AdyenPayment\AdyenApi\TransportFactory::checkout() */ public function getCheckout(): Checkout; } diff --git a/Components/Adyen/RefundService.php b/Components/Adyen/RefundService.php index 8585c0c2..a42f32c9 100644 --- a/Components/Adyen/RefundService.php +++ b/Components/Adyen/RefundService.php @@ -6,37 +6,24 @@ use Adyen\AdyenException; use Adyen\Service\Modification; +use AdyenPayment\AdyenApi\HttpClient\ClientMemoise; use AdyenPayment\Components\NotificationManager; use AdyenPayment\Models\Notification; use AdyenPayment\Models\PaymentInfo; use AdyenPayment\Models\Refund; +use Doctrine\ORM\EntityRepository; use Shopware\Components\Model\ModelManager; use Shopware\Models\Order\Order; class RefundService { - /** - * @var ApiClientMap - */ - private $apiClientMap; - - /** - * @var ModelManager - */ - private $modelManager; - - /** - * @var NotificationManager - */ - private $notificationManager; - - /** - * @var \Doctrine\ORM\EntityRepository|\Doctrine\Persistence\ObjectRepository - */ - private $paymentInfoRepository; + private ClientMemoise $apiClientMap; + private ModelManager $modelManager; + private NotificationManager $notificationManager; + private EntityRepository $paymentInfoRepository; public function __construct( - ApiClientMap $apiClientMap, + ClientMemoise $apiClientMap, ModelManager $modelManager, NotificationManager $notificationManager ) { diff --git a/Components/Configuration.php b/Components/Configuration.php index fff7d99c..582c76b8 100644 --- a/Components/Configuration.php +++ b/Components/Configuration.php @@ -11,7 +11,7 @@ use Shopware\Models\Shop\Shop; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; -final class Configuration +final class Configuration implements ConfigurationInterface { public const ENV_TEST = 'TEST'; public const ENV_LIVE = 'LIVE'; diff --git a/Components/ConfigurationInterface.php b/Components/ConfigurationInterface.php new file mode 100644 index 00000000..c9c33c02 --- /dev/null +++ b/Components/ConfigurationInterface.php @@ -0,0 +1,25 @@ +getPaymentInfo(); + $storedPaymentMethodId = (string) ($paymentInfo['storedPaymentMethodId'] ?? ''); + if ('' === $storedPaymentMethodId) { + return []; + } + + return [ + 'shopperInteraction' => ShopperInteraction::ecommerce()->shopperInteraction(), + 'recurringProcessingModel' => RecurringProcessingModel::cardOnFile()->recurringProcessingModel(), + ]; + } +} diff --git a/Components/Payload/Providers/RecurringPaymentProvider.php b/Components/Payload/Providers/RecurringPaymentProvider.php index b1c742e2..bea7d3d3 100644 --- a/Components/Payload/Providers/RecurringPaymentProvider.php +++ b/Components/Payload/Providers/RecurringPaymentProvider.php @@ -6,20 +6,22 @@ use AdyenPayment\Components\Payload\PaymentContext; use AdyenPayment\Components\Payload\PaymentPayloadProvider; +use AdyenPayment\Models\RecurringPayment\RecurringProcessingModel; +use AdyenPayment\Models\RecurringPayment\ShopperInteraction; -class RecurringPaymentProvider implements PaymentPayloadProvider +final class RecurringPaymentProvider implements PaymentPayloadProvider { public function provide(PaymentContext $context): array { $paymentInfo = $context->getPaymentInfo(); - $storedPaymentMethodId = $paymentInfo['storedPaymentMethodId'] ?? null; - if (!$storedPaymentMethodId) { + $storedPaymentMethodId = (string) ($paymentInfo['storedPaymentMethodId'] ?? ''); + if ('' === $storedPaymentMethodId) { return []; } return [ - 'shopperInteraction' => 'ContAuth', - 'recurringProcessingModel' => 'CardOnFile', + 'shopperInteraction' => ShopperInteraction::contAuth()->shopperInteraction(), + 'recurringProcessingModel' => RecurringProcessingModel::cardOnFile()->recurringProcessingModel(), ]; } } diff --git a/Controllers/Backend/TestAdyenApi.php b/Controllers/Backend/TestAdyenApi.php index 1ae672d3..2c293d12 100644 --- a/Controllers/Backend/TestAdyenApi.php +++ b/Controllers/Backend/TestAdyenApi.php @@ -1,6 +1,6 @@ cacheManager = $this->get(CacheManager::class); - $this->apiConfigValidator = $this->get(ApiConfigValidator::class); + $this->apiConfigValidator = $this->get(ConfigValidator::class); $this->usedFallbackConfigRule = $this->get(UsedFallbackConfigRule::class); } diff --git a/Controllers/Frontend/Adyen.php b/Controllers/Frontend/Adyen.php index 514bf469..54477383 100644 --- a/Controllers/Frontend/Adyen.php +++ b/Controllers/Frontend/Adyen.php @@ -4,9 +4,11 @@ use AdyenPayment\AdyenPayment; use AdyenPayment\Components\Adyen\PaymentMethodService; use AdyenPayment\Components\BasketService; +use AdyenPayment\Models\PaymentResultCode; use AdyenPayment\Components\Manager\AdyenManager; use AdyenPayment\Components\Payload\Chain; use AdyenPayment\Components\Payload\PaymentContext; +use AdyenPayment\Components\Payload\PaymentPayloadProvider; use AdyenPayment\Models\PaymentInfo; use Shopware\Components\Logger; use Shopware\Models\Order\Order; @@ -18,41 +20,22 @@ //phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace, Squiz.Classes.ValidClassName.NotCamelCaps class Shopware_Controllers_Frontend_Adyen extends Shopware_Controllers_Frontend_Payment { - /** - * @var AdyenManager - */ - private $adyenManager; - - /** - * @var PaymentMethodService - */ - private $adyenCheckout; - - /** - * @var BasketService - */ - private $basketService; - - /** - * @var Logger - */ - private $logger; - - /** - * @var Chain - */ - private $paymentPayloadProvider; + private AdyenManager $adyenManager; + private PaymentMethodService $adyenCheckout; + private Logger $logger; + private Chain $paymentPayloadProvider; + private BasketService $basketService; /** * @return void */ public function preDispatch() { - $this->adyenManager = $this->get('AdyenPayment\Components\Manager\AdyenManager'); - $this->adyenCheckout = $this->get('AdyenPayment\Components\Adyen\PaymentMethodService'); - $this->basketService = $this->get('AdyenPayment\Components\BasketService'); + $this->adyenManager = $this->get(AdyenManager::class); + $this->adyenCheckout = $this->get(PaymentMethodService::class); $this->logger = $this->get('adyen_payment.logger'); - $this->paymentPayloadProvider = $this->get('AdyenPayment\Components\Payload\PaymentPayloadProvider'); + $this->paymentPayloadProvider = $this->get(PaymentPayloadProvider::class); + $this->basketService = $this->get(BasketService::class); } public function ajaxDoPaymentAction(): void @@ -71,6 +54,7 @@ public function ajaxDoPaymentAction(): void $context->getTransaction(), $paymentInfo['paymentData'] ?? '' ); + $this->handlePaymentData($paymentInfo); $this->Response()->setBody(json_encode( @@ -127,7 +111,7 @@ public function paymentDetailsAction(): void $payload = array_intersect_key($this->Request()->getPost(), ['details' => true]); $checkout = $this->adyenCheckout->getCheckout(); $paymentInfo = $checkout->paymentsDetails($payload); - $this->handlePaymentData($paymentInfo); + ($this->paymentResultCodeHandler)($paymentInfo); $this->Response()->setBody(json_encode($paymentInfo)); } @@ -235,7 +219,6 @@ private function getShopperInfo(): array ]; } - /** * @param $paymentInfo * @@ -247,13 +230,11 @@ private function getShopperInfo(): array */ private function handlePaymentData($paymentInfo): void { - if (!in_array( - $paymentInfo['resultCode'], - ['Authorised', 'IdentifyShopper', 'ChallengeShopper', 'RedirectShopper'] - ) - ) { - $this->handlePaymentDataError($paymentInfo); + if (PaymentResultCode::exists((string) ($paymentInfo['resultCode'] ?? ''))) { + return; } + + $this->handlePaymentDataError($paymentInfo); } /** @@ -265,10 +246,10 @@ private function handlePaymentData($paymentInfo): void * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\OptimisticLockException */ - private function handlePaymentDataError($paymentInfo): void + private function handlePaymentDataError(array $paymentResponseInfo): void { - if ($paymentInfo['merchantReference']) { - $this->basketService->cancelAndRestoreByOrderNumber($paymentInfo['merchantReference']); + if (array_key_exists('merchantReference', $paymentResponseInfo)) { + $this->basketService->cancelAndRestoreByOrderNumber($paymentResponseInfo['merchantReference']); } } } diff --git a/Controllers/Frontend/DisableRecurringToken.php b/Controllers/Frontend/DisableRecurringToken.php new file mode 100644 index 00000000..9ff33d1c --- /dev/null +++ b/Controllers/Frontend/DisableRecurringToken.php @@ -0,0 +1,72 @@ +frontendJsonResponse = $this->get(FrontendJsonResponse::class); + $this->disableTokenRequestHandler = $this->get(DisableTokenRequestHandler::class); + } + + public function disabledAction(): void + { + try { + if (!$this->Request()->isPost()) { + $this->frontendJsonResponse->sendJsonBadRequestResponse( + $this->Front(), + $this->Response(), + 'Invalid method.' + ); + + return; + } + + $recurringToken = $this->Request()->getParams()['recurringToken'] ?? ''; + if ('' === $recurringToken) { + $this->frontendJsonResponse->sendJsonBadRequestResponse( + $this->Front(), + $this->Response(), + 'Missing recurring token param.' + ); + + return; + } + + $result = $this->disableTokenRequestHandler->disableToken($recurringToken, Shopware()->Shop()); + if (!$result->isSuccess()) { + $this->frontendJsonResponse->sendJsonResponse( + $this->Front(), + $this->Response(), + JsonResponse::create( + ['error' => true, 'message' => $result->message()], + Response::HTTP_BAD_REQUEST + ) + ); + + return; + } + + $this->frontendJsonResponse->sendJsonResponse( + $this->Front(), + $this->Response(), + JsonResponse::create(null, Response::HTTP_NO_CONTENT) + ); + } catch (\Exception $e) { + $this->frontendJsonResponse->sendJsonBadRequestResponse($this->Front(), $this->Response(), $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/Controllers/Frontend/Process.php b/Controllers/Frontend/Process.php index c79dc4eb..887cf4f8 100644 --- a/Controllers/Frontend/Process.php +++ b/Controllers/Frontend/Process.php @@ -4,9 +4,11 @@ use AdyenPayment\Components\Adyen\PaymentMethodService; use AdyenPayment\Components\BasketService; use AdyenPayment\Components\Manager\AdyenManager; +use AdyenPayment\Components\Manager\OrderManager; use AdyenPayment\Components\Manager\OrderManagerInterface; use AdyenPayment\Components\OrderMailService; -use AdyenPayment\Models\Enum\PaymentResultCodes; +use AdyenPayment\Models\PaymentResultCode; +use AdyenPayment\Session\ErrorMessageProvider; use AdyenPayment\Session\MessageProvider; use AdyenPayment\Utils\RequestDataFormatter; use Shopware\Components\CSRFWhitelistAware; @@ -43,14 +45,14 @@ public function getWhitelistedCSRFActions() */ public function preDispatch() { - $this->adyenManager = $this->get('AdyenPayment\Components\Manager\AdyenManager'); - $this->adyenCheckout = $this->get('AdyenPayment\Components\Adyen\PaymentMethodService'); - $this->basketService = $this->get('AdyenPayment\Components\BasketService'); - $this->orderMailService = $this->get('AdyenPayment\Components\OrderMailService'); + $this->adyenManager = $this->get(AdyenManager::class); + $this->adyenCheckout = $this->get(PaymentMethodService::class); + $this->basketService = $this->get(BasketService::class); + $this->orderMailService = $this->get(OrderMailService::class); $this->logger = $this->get('adyen_payment.logger'); - $this->orderManager = $this->get('AdyenPayment\Components\Manager\OrderManager'); + $this->orderManager = $this->get(OrderManager::class); $this->snippets = $this->get('snippets'); - $this->errorMessageProvider = $this->get('AdyenPayment\Session\ErrorMessageProvider'); + $this->errorMessageProvider = $this->get(ErrorMessageProvider::class); } /** @@ -70,10 +72,10 @@ public function returnAction(): void $result = $this->validateResponse($response, $order); $this->handleReturnResult($result, $order); - switch ($result['resultCode']) { - case PaymentResultCodes::AUTHORISED: - case PaymentResultCodes::PENDING: - case PaymentResultCodes::RECEIVED: + switch(PaymentResultCode::load($result['resultCode'])) { + case PaymentResultCode::authorised(): + case PaymentResultCode::pending(): + case PaymentResultCode::received(): if (!empty($result['merchantReference'])) { $this->orderMailService->sendOrderConfirmationMail($result['merchantReference']); } @@ -84,9 +86,9 @@ public function returnAction(): void 'sAGB' => true, ]); break; - case PaymentResultCodes::CANCELLED: - case PaymentResultCodes::ERROR: - case PaymentResultCodes::REFUSED: + case PaymentResultCode::cancelled(): + case PaymentResultCode::error(): + case PaymentResultCode::refused(): default: $this->errorMessageProvider->add( $this->snippets->getNamespace('adyen/checkout/error') @@ -121,18 +123,18 @@ private function handleReturnResult(array $result, ?Order $order): void return; } - switch ($result['resultCode']) { - case PaymentResultCodes::AUTHORISED: - case PaymentResultCodes::PENDING: - case PaymentResultCodes::RECEIVED: + switch (PaymentResultCode::load($result['resultCode'])) { + case PaymentResultCode::authorised(): + case PaymentResultCode::pending(): + case PaymentResultCode::received(): $paymentStatus = $this->getModelManager()->find( Status::class, Status::PAYMENT_STATE_THE_PAYMENT_HAS_BEEN_ORDERED ); break; - case PaymentResultCodes::CANCELLED: - case PaymentResultCodes::ERROR: - case PaymentResultCodes::REFUSED: + case PaymentResultCode::cancelled(): + case PaymentResultCode::error(): + case PaymentResultCode::refused(): $paymentStatus = $this->getModelManager()->find( Status::class, Status::PAYMENT_STATE_THE_PROCESS_HAS_BEEN_CANCELLED diff --git a/Exceptions/InvalidPaymentsResponseException.php b/Exceptions/InvalidPaymentsResponseException.php new file mode 100644 index 00000000..b03f9b8d --- /dev/null +++ b/Exceptions/InvalidPaymentsResponseException.php @@ -0,0 +1,13 @@ +resultCode(), + $pspReference + )); + } +} diff --git a/Exceptions/RecurringPaymentTokenNotSavedException.php b/Exceptions/RecurringPaymentTokenNotSavedException.php new file mode 100644 index 00000000..28839b50 --- /dev/null +++ b/Exceptions/RecurringPaymentTokenNotSavedException.php @@ -0,0 +1,15 @@ +identifier().'"'); + } +} diff --git a/Http/Response/ApiJsonResponse.php b/Http/Response/ApiJsonResponse.php new file mode 100644 index 00000000..7077a90f --- /dev/null +++ b/Http/Response/ApiJsonResponse.php @@ -0,0 +1,24 @@ +Plugins()->ViewRenderer()->setNoRender(); + + $httpResponse->setHeader('Content-type', $response->headers->get('Content-Type'), true); + $httpResponse->setHttpResponseCode($response->getStatusCode()); + $httpResponse->setBody($response->getContent()); + + return $httpResponse; + } + + public function sendJsonBadRequestResponse( + Enlight_Controller_Front $frontController, + Enlight_Controller_Response_ResponseHttp $httpResponse, + string $message + ): Enlight_Controller_Response_ResponseHttp { + return $this->sendJsonResponse( + $frontController, + $httpResponse, + JsonResponse::create( + ['error' => true, 'message' => $message], + Response::HTTP_BAD_REQUEST + ) + ); + } +} diff --git a/Models/Enum/PaymentResultCodes.php b/Models/Enum/PaymentResultCodes.php deleted file mode 100644 index 11b78062..00000000 --- a/Models/Enum/PaymentResultCodes.php +++ /dev/null @@ -1,18 +0,0 @@ -resultCode = $resultCode; + } + + public function resultCode(): string + { + return $this->resultCode; + } + + public function equals(PaymentResultCode $paymentResultCode): bool + { + return $paymentResultCode->resultCode() === $this->resultCode; + } + + public static function load(string $resultCode): self + { + return new self($resultCode); + } + + public static function exists(string $resultCode): bool + { + return in_array($resultCode, self::availableResultCodes(), true); + } + + public static function authorised(): self + { + return new self(self::AUTHORISED); + } + + public static function challengeShopper(): self + { + return new self(self::CHALLENGE_SHOPPER); + } + + public static function cancelled(): self + { + return new self(self::CANCELLED); + } + + public static function error(): self + { + return new self(self::ERROR); + } + + public static function invalid(): self + { + return new self(self::INVALID); + } + + public static function identifyShopper(): self + { + return new self(self::IDENTIFY_SHOPPER); + } + + public static function pending(): self + { + return new self(self::PENDING); + } + + public static function received(): self + { + return new self(self::RECEIVED); + } + + public static function redirectShopper(): self + { + return new self(self::REDIRECT_SHOPPER); + } + + public static function refused(): self + { + return new self(self::REFUSED); + } + + /** + * @return array + */ + private static function availableResultCodes(): array + { + return [ + self::AUTHORISED, + self::CANCELLED, + self::CHALLENGE_SHOPPER, + self::ERROR, + self::INVALID, + self::IDENTIFY_SHOPPER, + self::PENDING, + self::RECEIVED, + self::REDIRECT_SHOPPER, + self::REFUSED, + ]; + } +} diff --git a/Models/RecurringPayment/RecurringPaymentToken.php b/Models/RecurringPayment/RecurringPaymentToken.php new file mode 100644 index 00000000..ea45d5b4 --- /dev/null +++ b/Models/RecurringPayment/RecurringPaymentToken.php @@ -0,0 +1,191 @@ +setCreatedAt(new \DateTimeImmutable()); + $this->setUpdatedAt(new \DateTimeImmutable()); + } + + public static function create( + TokenIdentifier $id, + string $customerId, + string $recurringDetailReference, + string $pspReference, + string $orderNumber, + PaymentResultCode $resultCode, + int $amountValue, + string $amountCurrency + ): self { + $new = new self(); + $new->id = $id->identifier(); + $new->customerId = $customerId; + $new->recurringDetailReference = $recurringDetailReference; + $new->pspReference = $pspReference; + $new->orderNumber = $orderNumber; + $new->resultCode = $resultCode->resultCode(); + $new->amountValue = $amountValue; + $new->amountCurrency = $amountCurrency; + + return $new; + } + + /** + * @internal + * + * @see RecurringPaymentToken::tokenIdentifier() + */ + public function id(): string + { + return $this->id; + } + + public function tokenIdentifier(): TokenIdentifier + { + return TokenIdentifier::generateFromString($this->id); + } + + public function customerId(): string + { + return $this->customerId; + } + + public function recurringDetailReference(): string + { + return $this->recurringDetailReference; + } + + public function pspReference(): string + { + return $this->pspReference; + } + + public function orderNumber(): string + { + return $this->orderNumber; + } + + /** + * @internal + * + * @see RecurringPaymentToken::resultCode() + */ + public function getResultCode(): string + { + return $this->resultCode; + } + + public function resultCode(): PaymentResultCode + { + return PaymentResultCode::load($this->resultCode); + } + + public function amountValue(): int + { + return $this->amountValue; + } + + public function amountCurrency(): string + { + return $this->amountCurrency; + } + + public function createdAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): void + { + $this->createdAt = $createdAt; + } + + public function updatedAt(): \DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(\DateTimeImmutable $updatedAt): void + { + $this->updatedAt = $updatedAt; + } + + public function isSubscription(): bool + { + return '' === $this->orderNumber(); + } + + public function isOneOffPayment(): bool + { + return '' !== $this->orderNumber(); + } +} diff --git a/Models/RecurringPayment/RecurringProcessingModel.php b/Models/RecurringPayment/RecurringProcessingModel.php new file mode 100644 index 00000000..72c8b292 --- /dev/null +++ b/Models/RecurringPayment/RecurringProcessingModel.php @@ -0,0 +1,56 @@ +availableRecurringProcessingModels(), true)) { + throw new \InvalidArgumentException('Invalid recurring processing model: "'.$recurringProcessingModel.'"'); + } + + $this->recurringProcessingModel = $recurringProcessingModel; + } + + public function recurringProcessingModel(): string + { + return $this->recurringProcessingModel; + } + + public function equals(RecurringProcessingModel $recurringProcessingModel): bool + { + return $recurringProcessingModel->recurringProcessingModel() === $this->recurringProcessingModel; + } + + public static function load(string $recurringProcessingModel): self + { + return new self($recurringProcessingModel); + } + + public static function cardOnFile(): self + { + return new self(self::CARD_ON_FILE); + } + + public static function subscription(): self + { + return new self(self::SUBSCRIPTION); + } + + private function availableRecurringProcessingModels(): array + { + return [ + self::CARD_ON_FILE, + self::SUBSCRIPTION, + self::UNSCHEDULED_CARD_ON_FILE, + ]; + } +} diff --git a/Models/RecurringPayment/ShopperInteraction.php b/Models/RecurringPayment/ShopperInteraction.php new file mode 100644 index 00000000..6e8d0c72 --- /dev/null +++ b/Models/RecurringPayment/ShopperInteraction.php @@ -0,0 +1,58 @@ +availableShopperInteractions(), true)) { + throw new \InvalidArgumentException('Invalid shopper interaction: "'.$shopperInteraction.'"'); + } + + $this->shopperInteraction = $shopperInteraction; + } + + public function shopperInteraction(): string + { + return $this->shopperInteraction; + } + + public function equals(ShopperInteraction $paymentShopperInteraction): bool + { + return $paymentShopperInteraction->shopperInteraction() === $this->shopperInteraction; + } + + public static function load(string $shopperInteraction): self + { + return new self($shopperInteraction); + } + + public static function contAuth(): self + { + return new self(self::CONT_AUTH); + } + + public static function ecommerce(): self + { + return new self(self::ECOMMERCE); + } + + private function availableShopperInteractions(): array + { + return [ + self::CONT_AUTH, + self::ECOMMERCE, + self::MOTO, + self::POS, + ]; + } +} diff --git a/Models/TokenIdentifier.php b/Models/TokenIdentifier.php new file mode 100644 index 00000000..ffc5b5e5 --- /dev/null +++ b/Models/TokenIdentifier.php @@ -0,0 +1,38 @@ +tokenId = $tokenId; + } + + public static function generate(): TokenIdentifier + { + return new self(Uuid::uuid4()); + } + + public static function generateFromString(string $uuid): TokenIdentifier + { + return new self(Uuid::fromString($uuid)); + } + + public function identifier(): string + { + return $this->tokenId->toString(); + } + + public function equals(TokenIdentifier $id): bool + { + return $id->identifier() === $this->identifier(); + } +} diff --git a/Recurring/RecurringTokenFactory.php b/Recurring/RecurringTokenFactory.php new file mode 100644 index 00000000..02ec8b44 --- /dev/null +++ b/Recurring/RecurringTokenFactory.php @@ -0,0 +1,33 @@ +entityManager = $entityManager; + $this->recurringPaymentTokenEntityRepository = $recurringPaymentTokenEntityRepository; + } + + public function fetchByCustomerIdAndOrderNumber(string $customerId, string $orderNumber): RecurringPaymentToken + { + $recurringPaymentToken = $this->recurringPaymentTokenEntityRepository->findOneBy([ + 'customerId' => $customerId, + 'orderNumber' => $orderNumber, + ]); + + if (!($recurringPaymentToken instanceof RecurringPaymentToken)) { + throw RecurringPaymentTokenNotFoundException::withCustomerIdAndOrderNumber($customerId, $orderNumber); + } + + return $recurringPaymentToken; + } + + public function fetchPendingByPspReference(string $pspReference): RecurringPaymentToken + { + $recurringPaymentToken = $this->recurringPaymentTokenEntityRepository->findOneBy([ + 'resultCode' => PaymentResultCode::pending()->resultCode(), + 'pspReference' => $pspReference, + ]); + + if (!($recurringPaymentToken instanceof RecurringPaymentToken)) { + throw RecurringPaymentTokenNotFoundException::withPendingResultCodeAndPspReference($pspReference); + } + + return $recurringPaymentToken; + } + + public function update(RecurringPaymentToken $recurringPaymentToken): void + { + $this->entityManager->persist($recurringPaymentToken); + $this->entityManager->flush($recurringPaymentToken); + } +} diff --git a/Repository/RecurringPayment/RecurringPaymentTokenRepositoryInterface.php b/Repository/RecurringPayment/RecurringPaymentTokenRepositoryInterface.php new file mode 100644 index 00000000..0eb07313 --- /dev/null +++ b/Repository/RecurringPayment/RecurringPaymentTokenRepositoryInterface.php @@ -0,0 +1,14 @@ +recurringPaymentTokenRepository = $recurringPaymentTokenRepository; + $this->logger = $logger; + } + + public function fetchByCustomerIdAndOrderNumber(string $customerId, string $orderNumber): RecurringPaymentToken + { + try { + return $this->recurringPaymentTokenRepository->fetchByCustomerIdAndOrderNumber($customerId, $orderNumber); + } catch (RecurringPaymentTokenNotFoundException $exception) { + $this->logger->info($exception->getMessage(), ['exception' => $exception]); + + throw $exception; + } + } + + public function fetchPendingByPspReference(string $pspReference): RecurringPaymentToken + { + try { + return $this->recurringPaymentTokenRepository->fetchPendingByPspReference($pspReference); + } catch (RecurringPaymentTokenNotFoundException $exception) { + $this->logger->info($exception->getMessage(), ['exception' => $exception]); + + throw $exception; + } + } + + public function update(RecurringPaymentToken $recurringPaymentToken): void + { + try { + $this->recurringPaymentTokenRepository->update($recurringPaymentToken); + } catch (ORMException|ORMInvalidArgumentException $exception) { + $this->logger->error($exception->getMessage(), ['exception' => $exception]); + + throw RecurringPaymentTokenNotSavedException::withId($recurringPaymentToken->tokenIdentifier()); + } + } +} diff --git a/Resources/frontend/js/jquery.adyen-disable-payment.js b/Resources/frontend/js/jquery.adyen-disable-payment.js new file mode 100644 index 00000000..8bdf54a8 --- /dev/null +++ b/Resources/frontend/js/jquery.adyen-disable-payment.js @@ -0,0 +1,64 @@ +;(function ($) { + 'use strict'; + $.plugin('adyen-disable-payment', { + /** + * Plugin default options. + */ + defaults: { + adyenDisableTokenUrl: '', + adyenStoredMethodId: '', + /** + * Selector for the stored payment "disable" button. + * + * @type {String} + */ + disableTokenSelector: '[data-adyen-disable-payment]', + /** + * @var string errorClass + * CSS classes for the error element + */ + errorClass: 'alert is--error is--rounded is--adyen-error', + /** + * @var string errorClassSelector + * CSS classes selector to clear the error elements + */ + errorClassSelector: '.alert.is--error.is--rounded.is--adyen-error', + /** + * @var string errorMessageClass + * CSS classes for the error message element + */ + errorMessageClass: 'alert--content' + }, + init: function () { + var me = this; + me.applyDataAttributes(); + me.$el.on('click', $.proxy(me.enableDisableButtonClick, me)); + }, + enableDisableButtonClick: function () { + var me = this; + if (0 === me.opts.adyenStoredMethodId.length) { + return; + } + $.loadingIndicator.open(); + $.post({ + url: me.opts.adyenDisableTokenUrl, + dataType: 'json', + data: {recurringToken: me.opts.adyenStoredMethodId}, + success: function () { + window.location.reload(); + } + }).fail(function (response) { + me.appendError(response.responseJSON.message); + }).always(function () { + $.loadingIndicator.close(); + }); + }, + appendError: function (message) { + var me = this; + $(me.opts.errorClassSelector).remove(); + var error = $('
').addClass(me.opts.errorClass); + error.append($('
').addClass(me.opts.errorMessageClass).html(message)); + me.$el.parent().append(error); + } + }); +})(jQuery); diff --git a/Resources/frontend/js/jquery.plugin-loader.js b/Resources/frontend/js/jquery.plugin-loader.js index 1dc20fa9..8e2c14af 100644 --- a/Resources/frontend/js/jquery.plugin-loader.js +++ b/Resources/frontend/js/jquery.plugin-loader.js @@ -5,6 +5,7 @@ .addPlugin('.adyen-payment-selection', 'adyen-payment-selection') .addPlugin('*[data-adyen-checkout-error="true"]', 'adyen-checkout-error') .addPlugin('.is--act-confirm', 'adyen-confirm-order') - .addPlugin('.is--act-finish', 'adyen-finish-order'); + .addPlugin('.is--act-finish', 'adyen-finish-order') + .addPlugin('[data-adyen-disable-payment]', 'adyen-disable-payment'); }); })(jQuery); diff --git a/Resources/services/adyen-api.xml b/Resources/services/adyen-api.xml new file mode 100644 index 00000000..0239d3d0 --- /dev/null +++ b/Resources/services/adyen-api.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Resources/services/components.xml b/Resources/services/components.xml index 94f57b34..1512826a 100644 --- a/Resources/services/components.xml +++ b/Resources/services/components.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> @@ -26,22 +26,14 @@ - - - - - - - - + - - + - + @@ -176,7 +168,12 @@ - + + + + diff --git a/Resources/services/http.xml b/Resources/services/http.xml index 38a05be4..4088e3e3 100644 --- a/Resources/services/http.xml +++ b/Resources/services/http.xml @@ -3,6 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/services/providers.xml b/Resources/services/providers.xml index 5c7267d2..ad4a1fa6 100644 --- a/Resources/services/providers.xml +++ b/Resources/services/providers.xml @@ -5,7 +5,7 @@ - + diff --git a/Resources/services/repositories.xml b/Resources/services/repositories.xml index 4a1bd5fc..aae2f781 100644 --- a/Resources/services/repositories.xml +++ b/Resources/services/repositories.xml @@ -1,18 +1,27 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + service('models').getRepository('AdyenPayment\Models\RecurringPayment\RecurringPaymentToken') + + + + + + + - - - diff --git a/Resources/services/session.xml b/Resources/services/session.xml index 46893c43..cc51709a 100644 --- a/Resources/services/session.xml +++ b/Resources/services/session.xml @@ -5,5 +5,9 @@ + + + + diff --git a/Resources/services/validators.xml b/Resources/services/validators.xml index c6d9fd14..4311d685 100644 --- a/Resources/services/validators.xml +++ b/Resources/services/validators.xml @@ -1,18 +1,18 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + class="AdyenPayment\Http\Validator\Notification\Chain" public="true"> + decorates="AdyenPayment\Http\Validator\Notification\AuthorizationValidator" public="true"> + id="AdyenPayment\Http\Validator\Notification\LoggingAuthorizationValidatorDecorator.inner"/> @@ -24,9 +24,8 @@ - - - + + service('models').getRepository('Shopware\\Models\\Shop\\Shop') diff --git a/Resources/views/frontend/register/payment_fieldset.tpl b/Resources/views/frontend/register/payment_fieldset.tpl index b9667b9c..795d6fe9 100644 --- a/Resources/views/frontend/register/payment_fieldset.tpl +++ b/Resources/views/frontend/register/payment_fieldset.tpl @@ -16,3 +16,21 @@ )} checked="checked"{/if} /> {/block} + +{block name="frontend_register_payment_fieldset_description"} + {$smarty.block.parent} + + {block name="frontend_register_payment_stored_method_action_disable"} + {if $isStoredPayment } +
+ +
+ {/if} + {/block} +{/block} diff --git a/Session/CustomerNumberProvider.php b/Session/CustomerNumberProvider.php new file mode 100644 index 00000000..d93abf1a --- /dev/null +++ b/Session/CustomerNumberProvider.php @@ -0,0 +1,32 @@ +session = $session; + $this->modelManager = $modelManager; + } + + public function __invoke(): string + { + $userId = $this->session->get('sUserId'); + if (!$userId) { + return ''; + } + $customer = $this->modelManager->getRepository(Customer::class)->find($userId); + + return $customer ? (string) $customer->getNumber() : ''; + } +} diff --git a/Session/CustomerNumberProviderInterface.php b/Session/CustomerNumberProviderInterface.php new file mode 100644 index 00000000..d47396a6 --- /dev/null +++ b/Session/CustomerNumberProviderInterface.php @@ -0,0 +1,10 @@ +Adyen Shopware Plugin - 3.4.0 + 3.5.0 Adyen Adyen https://adyen.com @@ -305,4 +305,16 @@ Enable Adyen's stored payment methods feature + + + USP: + * re-enable creation of stored payment methods (Tokenization) for Credit Cards (adyen type: scheme) + * functionality to remove stored payment methods / tokens + + + USP: + * re-enable creation of stored payment methods (Tokenization) for Credit Cards (adyen type: scheme) + * functionality to remove stored payment methods / tokens + + diff --git a/tests/Integration/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php b/tests/Integration/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php new file mode 100644 index 00000000..ce2fa288 --- /dev/null +++ b/tests/Integration/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php @@ -0,0 +1,15 @@ +markTestIncomplete(); + } +} diff --git a/tests/Unit/AdyenApi/HttpClient/ClientFactoryTest.php b/tests/Unit/AdyenApi/HttpClient/ClientFactoryTest.php new file mode 100644 index 00000000..324021d9 --- /dev/null +++ b/tests/Unit/AdyenApi/HttpClient/ClientFactoryTest.php @@ -0,0 +1,83 @@ +configuration = $this->prophesize(ConfigurationInterface::class); + $this->logger = $this->prophesize(LoggerInterface::class); + $this->clientFactory = new ClientFactory($this->configuration->reveal(), $this->logger->reveal()); + } + + /** @test */ + public function it_is_a_client_factory(): void + { + $this->assertInstanceOf(ClientFactory::class, $this->clientFactory); + } + + /** @test */ + public function it_can_provide_a_client_for_test_environment(): void + { + $shop = $this->prophesize(Shop::class); + $shop->getId()->willReturn('shop-id'); + + $this->configuration->getMerchantAccount($shop)->willReturn($merchantAccount = 'mock-merchantAccount'); + $this->configuration->getApiKey($shop)->willReturn($apiKey = 'mock-apiKey'); + $this->configuration->getEnvironment($shop)->willReturn($environment = Environment::TEST); + $this->configuration->getApiUrlPrefix($shop)->willReturn('api-url-prefix'); + + $result = $this->clientFactory->provide($shop->reveal()); + + $this->assertInstanceOf(Client::class, $result); + $this->assertEquals($merchantAccount, $result->getConfig()->getMerchantAccount()); + $this->assertEquals($apiKey, $result->getConfig()->getXApiKey()); + $this->assertEquals($environment, $result->getConfig()->getEnvironment()); + $this->assertEquals(Client::ENDPOINT_TEST, $result->getConfig()->get('endpoint')); + $this->assertEquals($this->logger->reveal(), $result->getLogger()); + } + + /** @test */ + public function it_can_provide_a_client_for_live_environment(): void + { + $shop = $this->prophesize(Shop::class); + $shop->getId()->willReturn('shop-id'); + + $this->configuration->getMerchantAccount($shop)->willReturn($merchantAccount = 'mock-merchantAccount'); + $this->configuration->getApiKey($shop)->willReturn($apiKey = 'mock-apiKey'); + $this->configuration->getEnvironment($shop)->willReturn($environment = Environment::LIVE); + $this->configuration->getApiUrlPrefix($shop)->willReturn($urlPrefix = 'api-url-prefix'); + + $result = $this->clientFactory->provide($shop->reveal()); + + $this->assertInstanceOf(Client::class, $result); + $this->assertEquals($merchantAccount, $result->getConfig()->getMerchantAccount()); + $this->assertEquals($apiKey, $result->getConfig()->getXApiKey()); + $this->assertEquals($environment, $result->getConfig()->getEnvironment()); + $expectedEndpoint = Client::ENDPOINT_PROTOCOL.$urlPrefix.Client::ENDPOINT_LIVE_SUFFIX; + $this->assertEquals($expectedEndpoint, $result->getConfig()->get('endpoint')); + $this->assertEquals($this->logger->reveal(), $result->getLogger()); + } +} diff --git a/tests/Unit/AdyenApi/HttpClient/ClientMemoiseTest.php b/tests/Unit/AdyenApi/HttpClient/ClientMemoiseTest.php new file mode 100644 index 00000000..2df38881 --- /dev/null +++ b/tests/Unit/AdyenApi/HttpClient/ClientMemoiseTest.php @@ -0,0 +1,61 @@ +clientFactory = $this->prophesize(ClientFactoryInterface::class); + $this->clientMemoise = new ClientMemoise($this->clientFactory->reveal()); + } + + /** @test */ + public function it_is_a_client_memoise(): void + { + $this->assertInstanceOf(ClientMemoise::class, $this->clientMemoise); + } + + /** @test */ + public function it_can_lookup_a_client(): void + { + $shop = new Shop(); + $client = $this->prophesize(Client::class); + $this->clientFactory->provide($shop)->willReturn($client); + + $result = $this->clientMemoise->lookup($shop); + + $this->assertSame($client->reveal(), $result); + } + + /** @test */ + public function it_can_return_a_memoised_client(): void + { + $shop = new Shop(); + $client = $this->prophesize(Client::class); + + $this->clientFactory->provide($shop)->shouldBeCalledOnce()->willReturn($client); + + $firstResult = $this->clientMemoise->lookup($shop); + $result = $this->clientMemoise->lookup($shop); + + $this->assertSame($client->reveal(), $firstResult); + $this->assertSame($client->reveal(), $result); + } +} diff --git a/tests/Unit/AdyenApi/HttpClient/ConfigValidatorTest.php b/tests/Unit/AdyenApi/HttpClient/ConfigValidatorTest.php new file mode 100644 index 00000000..312d406d --- /dev/null +++ b/tests/Unit/AdyenApi/HttpClient/ConfigValidatorTest.php @@ -0,0 +1,157 @@ +adyenApiFactory = $this->prophesize(ClientFactoryInterface::class); + $this->configuration = $this->prophesize(ConfigurationInterface::class); + $this->shopRepository = $this->prophesize(ObjectRepository::class); + + $this->configValidator = new ConfigValidator( + $this->adyenApiFactory->reveal(), + $this->configuration->reveal(), + $this->shopRepository->reveal() + ); + } + + /** @test */ + public function it_is_a_config_validator(): void + { + $this->assertInstanceOf(ConfigValidator::class, $this->configValidator); + } + + /** @test */ + public function it_will_return_a_violation_if_shop_is_not_found(): void + { + $this->shopRepository->find($shopId = 123456)->willReturn(null); + + $result = $this->configValidator->validate($shopId); + + $this->assertInstanceOf(ConstraintViolationList::class, $result); + $this->assertCount(1, $result); + $this->assertEquals(ConstraintViolationFactory::create('Shop not found for ID "'.$shopId.'".'), $result->get(0)); + } + + /** @test */ + public function it_will_return_a_violation_if_the_api_key_was_not_configured(): void + { + $shop = $this->prophesize(Shop::class); + $shop->getId()->willReturn($shopId = 123456); + $this->shopRepository->find($shopId)->willReturn($shop->reveal()); + + $this->configuration->getApiKey($shop)->willReturn(''); + $this->configuration->getMerchantAccount($shop->reveal())->willReturn('merchantAccount'); + + $result = $this->configValidator->validate($shopId); + + $this->assertInstanceOf(ConstraintViolationList::class, $result); + $this->assertCount(1, $result); + $this->assertEquals(ConstraintViolationFactory::create('Missing configuration: API key.'), $result->get(0)); + } + + /** @test */ + public function it_will_return_a_violation_if_the_merchant_account_was_not_configured(): void + { + $shop = $this->prophesize(Shop::class); + $shop->getId()->willReturn($shopId = 123456); + $this->shopRepository->find($shopId)->willReturn($shop->reveal()); + + $this->configuration->getApiKey($shop->reveal())->willReturn('api-key'); + $this->configuration->getMerchantAccount($shop->reveal())->willReturn(''); + + $this->assertEquals( + new ConstraintViolationList([ + ConstraintViolationFactory::create('Missing configuration: merchant account.'), + ]), + $this->configValidator->validate($shopId) + ); + } + + /** @test */ + public function it_will_return_a_violation_on_api_adyen_exception(): void + { + $shop = $this->prophesize(Shop::class); + $shop->getId()->willReturn($shopId = 123456); + $this->shopRepository->find($shopId)->willReturn($shop->reveal()); + + $this->configuration->getApiKey($shop)->willReturn('api-key'); + $this->configuration->getMerchantAccount($shop)->willReturn('merchantAccount'); + $this->adyenApiFactory->provide($shop)->willThrow(AdyenException::class); + + $this->assertEquals( + new ConstraintViolationList([ + ConstraintViolationFactory::create('Adyen API failed, check error logs'), + ]), + $this->configValidator->validate($shopId) + ); + } + + /** @test */ + public function it_can_validate_a_config(): void + { + $shop = $this->prophesize(Shop::class); + $shop->getId()->willReturn($shopId = 123456); + + $this->configuration->getApiKey($shop->reveal())->willReturn('api-key'); + $this->configuration->getMerchantAccount($shop->reveal())->willReturn($merchantAccount = 'merchantAccount'); + + $client = $this->createClientMock(); + $this->shopRepository->find($shopId)->willReturn($shop->reveal()); + + $this->adyenApiFactory->provide($shop->reveal())->willReturn($client->reveal()); + $this->configuration->getMerchantAccount($shop->reveal())->willReturn($merchantAccount); + + $this->assertEquals(new ConstraintViolationList(), $this->configValidator->validate($shopId)); + } + + private function createClientMock(): ObjectProphecy + { + $config = $this->prophesize(Config::class); + $config->get(Argument::any())->willReturn(Environment::TEST); + $config->getInputType(Argument::any())->willReturn(''); + $httpClient = $this->prophesize(ClientInterface::class); + $httpClient->requestJson(Argument::cetera())->willReturn([]); + + $client = $this->prophesize(Client::class); + $client->getConfig()->willReturn($config->reveal()); + $client->getHttpClient()->willReturn($httpClient->reveal()); + $client->getApiCheckoutVersion()->willReturn(''); + $client->getApiRecurringVersion()->willReturn(''); + + return $client; + } +} diff --git a/tests/Unit/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php b/tests/Unit/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php new file mode 100644 index 00000000..50d9f2e7 --- /dev/null +++ b/tests/Unit/AdyenApi/Recurring/DisableTokenRequestHandlerTest.php @@ -0,0 +1,103 @@ +customerNumberProvider = $this->prophesize(CustomerNumberProviderInterface::class); + $this->transportFactory = $this->prophesize(TransportFactoryInterface::class); + + $this->disableTokenRequestHandler = new DisableTokenRequestHandler( + $this->transportFactory->reveal(), + $this->customerNumberProvider->reveal() + ); + } + + /** @test */ + public function it_is_a_disable_token_request_handler(): void + { + $this->assertInstanceOf(DisableTokenRequestHandlerInterface::class, $this->disableTokenRequestHandler); + } + + /** @test */ + public function it_will_return_a_400_on_missing_customer_number(): void + { + $shop = $this->prophesize(Shop::class); + $this->customerNumberProvider->__invoke()->willReturn(''); + $this->transportFactory->recurring(Argument::any())->shouldNotBeCalled(); + + $result = $this->disableTokenRequestHandler->disableToken('recurringTokenId', $shop->reveal()); + + $this->assertEquals(ApiResponse::empty(), $result); + } + + /** @test */ + public function it_will_return_an_api_response_for_disable_token_success(): void + { + $shop = $this->prophesize(Shop::class); + $recurringTransport = $this->prophesize(Recurring::class); + $payload = [ + 'shopperReference' => $customerNumber = 'customer-number', + 'recurringDetailReference' => $recurringTokenId = 'recurringTokenId', + ]; + $this->customerNumberProvider->__invoke()->willReturn($customerNumber); + $this->transportFactory->recurring($shop)->willReturn($recurringTransport); + $recurringTransport->disable($payload)->willReturn([ + 'response' => 'successfully-disabled', + 'message' => $message = 'successfully-disabled', + ]); + + $result = $this->disableTokenRequestHandler->disableToken($recurringTokenId, $shop->reveal()); + + $this->assertInstanceOf(ApiResponse::class, $result); + $this->assertTrue($result->isSuccess()); + $this->assertEquals($message, $result->message()); + } + + /** @test */ + public function it_will_return_an_api_response_for_disable_token_error(): void + { + $shop = $this->prophesize(Shop::class); + $recurringTransport = $this->prophesize(Recurring::class); + $payload = [ + 'shopperReference' => $customerNumber = 'customer-number', + 'recurringDetailReference' => $recurringTokenId = 'recurringTokenId', + ]; + $this->customerNumberProvider->__invoke()->willReturn($customerNumber); + $this->transportFactory->recurring($shop)->willReturn($recurringTransport); + $recurringTransport->disable($payload)->willReturn([ + 'message' => $message = 'PaymentDetail not found', + ]); + + $result = $this->disableTokenRequestHandler->disableToken($recurringTokenId, $shop->reveal()); + + $this->assertInstanceOf(ApiResponse::class, $result); + $this->assertFalse($result->isSuccess()); + $this->assertEquals($message, $result->message()); + } +} diff --git a/tests/Unit/AdyenApi/TransportFactoryTest.php b/tests/Unit/AdyenApi/TransportFactoryTest.php new file mode 100644 index 00000000..038ec72d --- /dev/null +++ b/tests/Unit/AdyenApi/TransportFactoryTest.php @@ -0,0 +1,84 @@ +clientFactory = $this->prophesize(ClientFactoryInterface::class); + $this->transportFactory = new TransportFactory($this->clientFactory->reveal()); + } + + /** @test */ + public function it_is_a_transport_factory(): void + { + $this->assertInstanceOf(TransportFactoryInterface::class, $this->transportFactory); + } + + /** @test */ + public function it_can_provide_a_recurring_transport(): void + { + $shop = new Shop(); + $adyenClient = $this->createClientMock(); + + $this->clientFactory->provide($shop)->willReturn($adyenClient); + + $result = $this->transportFactory->recurring($shop); + + $this->assertInstanceOf(Recurring::class, $result); + } + + /** @test */ + public function it_can_provide_a_checkout_transport(): void + { + $shop = new Shop(); + $adyenClient = $this->createClientMock(); + + $this->clientFactory->provide($shop)->willReturn($adyenClient); + + $result = $this->transportFactory->checkout($shop); + + $this->assertInstanceOf(Checkout::class, $result); + } + + private function createClientMock(): ObjectProphecy + { + $config = $this->prophesize(Config::class); + $config->get(Argument::any())->willReturn(Environment::TEST); + $config->getInputType(Argument::any())->willReturn(''); + $httpClient = $this->prophesize(ClientInterface::class); + $httpClient->requestJson(Argument::cetera())->willReturn([]); + + $client = $this->prophesize(Client::class); + $client->getConfig()->willReturn($config->reveal()); + $client->getHttpClient()->willReturn($httpClient->reveal()); + $client->getApiCheckoutVersion()->willReturn(''); + $client->getApiRecurringVersion()->willReturn(''); + + return $client; + } +} diff --git a/tests/Unit/Components/Payload/Providers/RecurringOneOffPaymentTokenProviderTest.php b/tests/Unit/Components/Payload/Providers/RecurringOneOffPaymentTokenProviderTest.php new file mode 100644 index 00000000..ceb4d73e --- /dev/null +++ b/tests/Unit/Components/Payload/Providers/RecurringOneOffPaymentTokenProviderTest.php @@ -0,0 +1,58 @@ +recurringOneOffPaymentTokenProvider = new RecurringOneOffPaymentTokenProvider(); + $this->paymentContext = $this->prophesize(PaymentContext::class); + } + + /** @test */ + public function it_is_a_recurring_payment_payload_provider(): void + { + self::assertInstanceOf(PaymentPayloadProvider::class, $this->recurringOneOffPaymentTokenProvider); + } + + /** @test */ + public function it_will_return_empty_for_none_stored_payment_method(): void + { + $this->paymentContext->getPaymentInfo()->willReturn([]); + + $result = $this->recurringOneOffPaymentTokenProvider->provide($this->paymentContext->reveal()); + + self::assertEquals([], $result); + } + + /** @test */ + public function it_can_return_the_recurring_one_off_payment_token_data(): void + { + $this->paymentContext->getPaymentInfo()->willReturn(['storedPaymentMethodId' => 'stored-method-id']); + + $result = $this->recurringOneOffPaymentTokenProvider->provide($this->paymentContext->reveal()); + + self::assertEquals([ + 'shopperInteraction' => ShopperInteraction::ecommerce()->shopperInteraction(), + 'recurringProcessingModel' => RecurringProcessingModel::cardOnFile()->recurringProcessingModel(), + ], $result); + } +} diff --git a/tests/Unit/Components/Payload/Providers/RecurringPaymentProviderTest.php b/tests/Unit/Components/Payload/Providers/RecurringPaymentProviderTest.php new file mode 100644 index 00000000..197e2f7a --- /dev/null +++ b/tests/Unit/Components/Payload/Providers/RecurringPaymentProviderTest.php @@ -0,0 +1,58 @@ +recurringPaymentProvider = new RecurringPaymentProvider(); + $this->paymentContext = $this->prophesize(PaymentContext::class); + } + + /** @test */ + public function it_is_a_recurring_payment_payload_provider(): void + { + self::assertInstanceOf(PaymentPayloadProvider::class, $this->recurringPaymentProvider); + } + + /** @test */ + public function it_will_return_empty_for_none_stored_payment_method(): void + { + $this->paymentContext->getPaymentInfo()->willReturn([]); + + $result = $this->recurringPaymentProvider->provide($this->paymentContext->reveal()); + + self::assertEquals([], $result); + } + + /** @test */ + public function it_can_return_the_recurring_one_off_payment_token_data(): void + { + $this->paymentContext->getPaymentInfo()->willReturn(['storedPaymentMethodId' => 'stored-method-id']); + + $result = $this->recurringPaymentProvider->provide($this->paymentContext->reveal()); + + self::assertEquals([ + 'shopperInteraction' => ShopperInteraction::contAuth()->shopperInteraction(), + 'recurringProcessingModel' => RecurringProcessingModel::cardOnFile()->recurringProcessingModel(), + ], $result); + } +} diff --git a/tests/Unit/Exceptions/RecurringPaymentTokenNotFoundExceptionTest.php b/tests/Unit/Exceptions/RecurringPaymentTokenNotFoundExceptionTest.php new file mode 100644 index 00000000..ee859bbe --- /dev/null +++ b/tests/Unit/Exceptions/RecurringPaymentTokenNotFoundExceptionTest.php @@ -0,0 +1,55 @@ +exception = new RecurringPaymentTokenNotFoundException(); + } + + /** @test */ + public function is_a_runtime_exception(): void + { + self::assertInstanceOf(\RuntimeException::class, $this->exception); + } + + /** @test */ + public function it_can_be_constructed_with_customer_id_and_order_number(): void + { + $exception = RecurringPaymentTokenNotFoundException::withCustomerIdAndOrderNumber( + $customerId = 'customer-id', + $orderNumber = 'order-number' + ); + + self::assertInstanceOf(RecurringPaymentTokenNotFoundException::class, $exception); + self::assertEquals( + 'Recurring payment token not found with customer id: "'.$customerId.'", order number: "'.$orderNumber.'"', + $exception->getMessage() + ); + } + + /** @test */ + public function it_can_be_constructed_with_psp_reference(): void + { + $exception = RecurringPaymentTokenNotFoundException::withPendingResultCodeAndPspReference( + $pspReference = 'psp-reference' + ); + + self::assertInstanceOf(RecurringPaymentTokenNotFoundException::class, $exception); + self::assertEquals( + 'Recurring payment token not found with result code: "'.PaymentResultCode::pending()->resultCode() + .'", psp reference: "'.$pspReference.'"', + $exception->getMessage() + ); + } +} diff --git a/tests/Unit/Exceptions/RecurringPaymentTokenNotSavedExceptionTest.php b/tests/Unit/Exceptions/RecurringPaymentTokenNotSavedExceptionTest.php new file mode 100644 index 00000000..8e6e09a6 --- /dev/null +++ b/tests/Unit/Exceptions/RecurringPaymentTokenNotSavedExceptionTest.php @@ -0,0 +1,39 @@ +exception = new RecurringPaymentTokenNotSavedException(); + } + + /** @test */ + public function is_a_runtime_exception(): void + { + self::assertInstanceOf(\RuntimeException::class, $this->exception); + } + + /** @test */ + public function it_can_be_constructed_with_token_identifier(): void + { + $tokenIdentifier = TokenIdentifier::generate(); + + $exception = RecurringPaymentTokenNotSavedException::withId($tokenIdentifier); + + self::assertInstanceOf(RecurringPaymentTokenNotSavedException::class, $exception); + self::assertEquals( + 'Recurring payment token not saved with id: "'.$tokenIdentifier->identifier().'"', + $exception->getMessage() + ); + } +} diff --git a/tests/Unit/Http/Response/FrontendJsonResponseTest.php b/tests/Unit/Http/Response/FrontendJsonResponseTest.php new file mode 100644 index 00000000..a91b54b5 --- /dev/null +++ b/tests/Unit/Http/Response/FrontendJsonResponseTest.php @@ -0,0 +1,55 @@ +apiJsonResponse = new FrontendJsonResponse(); + } + + /** @test */ + public function it_is_an_api_json_response(): void + { + self::assertInstanceOf(ApiJsonResponse::class, $this->apiJsonResponse); + } + + /** @test */ + public function it_can_send_a_json_response(): void + { + $frontController = $this->prophesize(\Enlight_Controller_Front::class); + $httpResponse = $this->prophesize(\Enlight_Controller_Response_ResponseHttp::class); + $response = new JsonResponse([], Response::HTTP_OK); + $plugins = $this->prophesize(\Enlight_Plugin_Namespace_Loader::class); + $viewRenderer = $this->prophesize(\Enlight_Controller_Plugins_ViewRenderer_Bootstrap::class); + + $viewRenderer->setNoRender()->shouldBeCalled(); + $plugins->ViewRenderer()->willReturn($viewRenderer); + $frontController->Plugins()->willReturn($plugins); + + $httpResponse->setHeader('Content-type', $response->headers->get('Content-Type'), true)->shouldBeCalled(); + $httpResponse->setHttpResponseCode(Response::HTTP_OK)->shouldBeCalled(); + $httpResponse->setBody($response->getContent())->shouldBeCalled(); + + $result = $this->apiJsonResponse->sendJsonResponse( + $frontController->reveal(), + $httpResponse->reveal(), + $response + ); + + self::assertSame($httpResponse->reveal(), $result); + } +} diff --git a/tests/Unit/Models/PaymentResultCodeTest.php b/tests/Unit/Models/PaymentResultCodeTest.php new file mode 100644 index 00000000..f3d9f055 --- /dev/null +++ b/tests/Unit/Models/PaymentResultCodeTest.php @@ -0,0 +1,90 @@ +paymentResultCode = PaymentResultCode::authorised(); + } + + /** @test */ + public function it_knows_when_it_equals_payment_result_codes_objects(): void + { + $this->assertTrue($this->paymentResultCode->equals(PaymentResultCode::authorised())); + $this->assertFalse($this->paymentResultCode->equals(PaymentResultCode::invalid())); + } + + /** @test */ + public function it_is_immutable_constructed(): void + { + $paymentResultCodeAuthorised = PaymentResultCode::authorised(); + $this->assertEquals($this->paymentResultCode, $paymentResultCodeAuthorised); + $this->assertNotSame($this->paymentResultCode, $paymentResultCodeAuthorised); + } + + /** @test */ + public function it_throws_an_invalid_argument_exception_when_result_code_is_unknown(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid result code: "INVALID_CODE"'); + + PaymentResultCode::load('INVALID_CODE'); + } + + /** @test */ + public function it_can_load_a_result_code(): void + { + $this->assertEquals( + PaymentResultCode::authorised(), + PaymentResultCode::load('Authorised') + ); + } + + /** @test */ + public function it_knows_when_a_result_code_exists(): void + { + $result = PaymentResultCode::exists('Authorised'); + + $this->assertTrue($result); + } + + /** @test */ + public function it_knows_when_a_result_code_doesnt_exists(): void + { + $result = PaymentResultCode::exists('invalid-code-test'); + + $this->assertFalse($result); + } + + /** + * @dataProvider resultCodeProvider + * @test + */ + public function it_can_be_constructed_with_named_constructors(PaymentResultCode $resultCode, string $code): void + { + $this->assertEquals($code, $resultCode->resultCode()); + } + + public function resultCodeProvider(): \Generator + { + yield [PaymentResultCode::authorised(), 'Authorised']; + yield [PaymentResultCode::cancelled(), 'Cancelled']; + yield [PaymentResultCode::challengeShopper(), 'ChallengeShopper']; + yield [PaymentResultCode::error(), 'Error']; + yield [PaymentResultCode::invalid(), 'Invalid']; + yield [PaymentResultCode::identifyShopper(), 'IdentifyShopper']; + yield [PaymentResultCode::pending(), 'Pending']; + yield [PaymentResultCode::received(), 'Received']; + yield [PaymentResultCode::redirectShopper(), 'RedirectShopper']; + yield [PaymentResultCode::refused(), 'Refused']; + } +} diff --git a/tests/Unit/Models/RecurringPayment/RecurringPaymentTokenTest.php b/tests/Unit/Models/RecurringPayment/RecurringPaymentTokenTest.php new file mode 100644 index 00000000..325a9785 --- /dev/null +++ b/tests/Unit/Models/RecurringPayment/RecurringPaymentTokenTest.php @@ -0,0 +1,144 @@ +recurringPaymentToken = RecurringPaymentToken::create( + $tokenIdentifier = TokenIdentifier::generateFromString($knownUuid = '033a6dad-5a58-4b74-b420-6772bab3946e'), + $customerId = 'YOUR_UNIQUE_SHOPPER_ID_IOfW3k9G2PvXFu2j', + $recurringDetailReference = '8415698462516992', + $pspReference = '8515815919501547', + $orderNumber = 'YOUR_ORDER_NUMBER', + $resultCode = PaymentResultCode::authorised(), + $amountValue = 10500, + $amountCurrency = 'EUR' + ); + } + + /** @test */ + public function it_is_a_model_entity(): void + { + $this->assertInstanceOf(ModelEntity::class, $this->recurringPaymentToken); + } + + /** @test */ + public function it_contains_an_id(): void + { + $this->assertEquals('033a6dad-5a58-4b74-b420-6772bab3946e', $this->recurringPaymentToken->id()); + } + + /** @test */ + public function it_contains_a_token_identifier(): void + { + $this->assertEquals( + TokenIdentifier::generateFromString('033a6dad-5a58-4b74-b420-6772bab3946e'), + $this->recurringPaymentToken->tokenIdentifier() + ); + } + + /** @test */ + public function it_contains_a_customer_id(): void + { + $this->assertEquals('YOUR_UNIQUE_SHOPPER_ID_IOfW3k9G2PvXFu2j', $this->recurringPaymentToken->customerId()); + } + + /** @test */ + public function it_contains_a_recurring_detail_reference(): void + { + $this->assertEquals('8415698462516992', $this->recurringPaymentToken->recurringDetailReference()); + } + + /** @test */ + public function it_contains_a_psp_reference(): void + { + $this->assertEquals('8515815919501547', $this->recurringPaymentToken->pspReference()); + } + + /** @test */ + public function it_contains_an_order_number(): void + { + $this->assertEquals('YOUR_ORDER_NUMBER', $this->recurringPaymentToken->orderNumber()); + } + + /** @test */ + public function it_contains_a_result_code_string(): void + { + $this->assertEquals('Authorised', $this->recurringPaymentToken->getResultCode()); + } + + /** @test */ + public function it_contains_a_result_code(): void + { + $this->assertEquals(PaymentResultCode::load('Authorised'), $this->recurringPaymentToken->resultCode()); + } + + /** @test */ + public function it_contains_an_amount_value(): void + { + $this->assertEquals(10500, $this->recurringPaymentToken->amountValue()); + } + + /** @test */ + public function it_contains_an_amount_currency(): void + { + $this->assertEquals('EUR', $this->recurringPaymentToken->amountCurrency()); + } + + /** @test */ + public function it_contains_a_created_at_timestamp(): void + { + $createdAt = new \DateTimeImmutable(); + $this->recurringPaymentToken->setCreatedAt($createdAt); + $this->assertInstanceOf(\DateTimeImmutable::class, $this->recurringPaymentToken->createdAt()); + $this->assertStringContainsString( + $createdAt->format('d/m/y H:i'), + $this->recurringPaymentToken->createdAt()->format('d/m/y H:i') + ); + } + + /** @test */ + public function it_contains_an_updated_at_timestamp(): void + { + $updatedAt = new \DateTimeImmutable(); + $this->assertInstanceOf(\DateTimeImmutable::class, $this->recurringPaymentToken->updatedAt()); + $this->assertStringContainsString( + $updatedAt->format('d/m/y'), + $this->recurringPaymentToken->updatedAt()->format('d/m/y') + ); + } + + /** @test */ + public function it_knows_when_it_is_a_one_off_payment(): void + { + $this->assertTrue($this->recurringPaymentToken->isOneOffPayment()); + } + + /** @test */ + public function it_knows_when_it_is_a_subscription(): void + { + $recurringPaymentTokenOrderNumberEmpty = RecurringPaymentToken::create( + TokenIdentifier::generateFromString($uuid = 'f958e8a5-c707-4901-91dd-0e16b22b898c'), + 'YOUR_UNIQUE_SHOPPER_ID_IOfW3k9G2PvXFu2j', + '8415698462516992', + '8515815919501547', + $orderNumber = '', + PaymentResultCode::authorised(), + 10500, + 'EUR' + ); + $this->assertTrue($recurringPaymentTokenOrderNumberEmpty->isSubscription()); + } +} diff --git a/tests/Unit/Models/RecurringPayment/RecurringProcessingModelTest.php b/tests/Unit/Models/RecurringPayment/RecurringProcessingModelTest.php new file mode 100644 index 00000000..861df387 --- /dev/null +++ b/tests/Unit/Models/RecurringPayment/RecurringProcessingModelTest.php @@ -0,0 +1,73 @@ +recurringProcessingModel = RecurringProcessingModel::cardOnFile(); + } + + /** @test */ + public function it_contains_a_recurring_processing_model(): void + { + $this->assertInstanceOf(RecurringProcessingModel::class, $this->recurringProcessingModel); + } + + /** @test */ + public function it_knows_when_it_equals_a_processing_model(): void + { + $this->assertTrue($this->recurringProcessingModel->equals(RecurringProcessingModel::cardOnFile())); + $this->assertFalse($this->recurringProcessingModel->equals(RecurringProcessingModel::subscription())); + } + + /** @test */ + public function it_knows_when_it_equals_a_recurring_processing_model(): void + { + $recurringProcessingModel = RecurringProcessingModel::cardOnFile(); + $this->assertEquals($this->recurringProcessingModel, $recurringProcessingModel); + $this->assertNotSame($this->recurringProcessingModel, $recurringProcessingModel); + } + + /** @test */ + public function it_throws_an_invalid_argument_exception_when_recurring_processing_model_is_unknown(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid recurring processing model: "test"'); + + RecurringProcessingModel::load('test'); + } + + /** @test */ + public function it_can_load_a_recurring_processing_model(): void + { + $this->assertEquals( + RecurringProcessingModel::cardOnFile(), + RecurringProcessingModel::load('CardOnFile') + ); + } + + /** + * @dataProvider recurringProcessingModelProvider + * @test + */ + public function it_contains_recurring_processing_model( + RecurringProcessingModel $recurringProcessingModel, string $expected + ): void { + $this->assertEquals($expected, $recurringProcessingModel->recurringProcessingModel()); + } + + public function recurringProcessingModelProvider(): \Generator + { + yield [RecurringProcessingModel::cardOnFile(), 'CardOnFile']; + yield [RecurringProcessingModel::subscription(), 'Subscription']; + } +} diff --git a/tests/Unit/Models/RecurringPayment/ShopperInteractionTest.php b/tests/Unit/Models/RecurringPayment/ShopperInteractionTest.php new file mode 100644 index 00000000..46e9ac55 --- /dev/null +++ b/tests/Unit/Models/RecurringPayment/ShopperInteractionTest.php @@ -0,0 +1,70 @@ +shopperInteraction = ShopperInteraction::contAuth(); + } + + /** @test */ + public function it_contains_a_shopper_interaction(): void + { + $this->assertInstanceOf(ShopperInteraction::class, $this->shopperInteraction); + } + + /** @test */ + public function it_knows_when_it_equals_a_shopper_interaction(): void + { + $this->assertTrue($this->shopperInteraction->equals(ShopperInteraction::contAuth())); + $this->assertFalse($this->shopperInteraction->equals(ShopperInteraction::ecommerce())); + } + + /** @test */ + public function it_is_immutable_constructed(): void + { + $shopperInteractionContAuth = ShopperInteraction::contAuth(); + $this->assertEquals($this->shopperInteraction, $shopperInteractionContAuth); + $this->assertNotSame($this->shopperInteraction, $shopperInteractionContAuth); + } + + /** + * @dataProvider shopperInteractionProvider + * @test + */ + public function it_can_be_constructed_with_named_constructors(ShopperInteraction $shopperInteraction, string $expected): void + { + $this->assertEquals($expected, $shopperInteraction->shopperInteraction()); + } + + public function shopperInteractionProvider(): \Generator + { + yield [ShopperInteraction::contAuth(), 'ContAuth']; + yield [ShopperInteraction::ecommerce(), 'Ecommerce']; + } + + /** @test */ + public function it_throws_an_invalid_argument_exception_when_shopper_interaction_is_unknown(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid shopper interaction: "test"'); + + ShopperInteraction::load('test'); + } + + /** @test */ + public function it_can_load_a_shopper_interaction(): void + { + $result = ShopperInteraction::ecommerce(); + $this->assertEquals(ShopperInteraction::ecommerce(), $result); + } +} diff --git a/tests/Unit/Models/TokenIdentifierTest.php b/tests/Unit/Models/TokenIdentifierTest.php new file mode 100644 index 00000000..122388ba --- /dev/null +++ b/tests/Unit/Models/TokenIdentifierTest.php @@ -0,0 +1,55 @@ +tokenIdentifier = TokenIdentifier::generateFromString('3a2ee0d3-adc0-4386-869d-429b6d5f1fa0'); + } + + /** @test */ + public function it_contains_a_token_identifier(): void + { + $this->assertInstanceOf(TokenIdentifier::class, $this->tokenIdentifier); + } + + /** @test */ + public function it_knows_when_it_equals_token_identifier_objects(): void + { + $this->assertTrue($this->tokenIdentifier->equals(TokenIdentifier::generateFromString('3a2ee0d3-adc0-4386-869d-429b6d5f1fa0'))); + $this->assertFalse($this->tokenIdentifier->equals(TokenIdentifier::generate())); + } + + /** @test */ + public function it_constructs_immutable(): void + { + $tokenIdentifier = TokenIdentifier::generateFromString('3a2ee0d3-adc0-4386-869d-429b6d5f1fa0'); + $this->assertEquals($this->tokenIdentifier, $tokenIdentifier); + $this->assertNotSame($this->tokenIdentifier, $tokenIdentifier); + } + + /** @test */ + public function it_can_be_constructed_from_string(): void + { + $tokenIdentifier = TokenIdentifier::generateFromString($expected = 'af55ecab-90db-4501-ba7d-9eef61ac3ee3'); + + $this->assertEquals($expected, $tokenIdentifier->identifier()); + } + + /** @test */ + public function it_can_be_constructed_with_named_constructor(): void + { + $tokenIdentifier = TokenIdentifier::generate(); + + $this->assertInstanceOf(TokenIdentifier::class, $tokenIdentifier); + } +} diff --git a/tests/Unit/Recurring/RecurringTokenFactoryTest.php b/tests/Unit/Recurring/RecurringTokenFactoryTest.php new file mode 100644 index 00000000..1f422b2f --- /dev/null +++ b/tests/Unit/Recurring/RecurringTokenFactoryTest.php @@ -0,0 +1,89 @@ +recurringTokenMapper = new RecurringTokenFactory(); + } + + /** @test */ + public function it_is_a_recurring_token_mapper(): void + { + $this->assertInstanceOf(RecurringTokenFactoryInterface::class, $this->recurringTokenMapper); + } + + /** @test */ + public function it_throws_invalid_payments_response_exception(): void + { + $this->expectException(InvalidPaymentsResponseException::class); + $this->expectExceptionMessage('Empty Payment data.'); + + RecurringTokenFactory::create([]); + } + + /** @test */ + public function it_can_map_from_array(): void + { + $adyenPaymentsResponseArray = [ + 'additionalData' => [ + 'recurring.recurringDetailReference' => '8415698462516992', + 'recurring.shopperReference' => 'YOUR_UNIQUE_SHOPPER_ID_IOfW3k9G2PvXFu2j', + ], + 'pspReference' => '8515815919501547', + 'resultCode' => 'Authorised', + 'amount' => [ + 'currency' => 'USD', + 'value' => 0, + ], + 'merchantReference' => 'YOUR_ORDER_NUMBER', + ]; + $recurringPaymentToken = RecurringTokenFactory::create($adyenPaymentsResponseArray); + + $this->assertEquals('YOUR_UNIQUE_SHOPPER_ID_IOfW3k9G2PvXFu2j', $recurringPaymentToken->customerId()); + $this->assertEquals('8415698462516992', $recurringPaymentToken->recurringDetailReference()); + $this->assertEquals('8515815919501547', $recurringPaymentToken->pspReference()); + $this->assertEquals('YOUR_ORDER_NUMBER', $recurringPaymentToken->orderNumber()); + $this->assertEquals(PaymentResultCode::load('Authorised'), $recurringPaymentToken->resultCode()); + $this->assertIsInt($recurringPaymentToken->amountValue()); + $this->assertEquals(0, $recurringPaymentToken->amountValue()); + $this->assertEquals('USD', $recurringPaymentToken->amountCurrency()); + } + + /** @test */ + public function it_can_map_default_values(): void + { + $adyenPaymentsResponseArray = [ + 'additionalData' => [ + ], + 'amount' => [ + ], + ]; + $recurringPaymentToken = RecurringTokenFactory::create($adyenPaymentsResponseArray); + + $this->assertEquals('', $recurringPaymentToken->customerId()); + $this->assertEquals('', $recurringPaymentToken->recurringDetailReference()); + $this->assertEquals('', $recurringPaymentToken->pspReference()); + $this->assertEquals('', $recurringPaymentToken->orderNumber()); + $this->assertEquals(PaymentResultCode::load('Invalid'), $recurringPaymentToken->resultCode()); + $this->assertEquals(0, $recurringPaymentToken->amountValue()); + $this->assertEquals('', $recurringPaymentToken->amountCurrency()); + } +} diff --git a/tests/Unit/Repository/RecurringPayment/RecurringPaymentTokenRepositoryTest.php b/tests/Unit/Repository/RecurringPayment/RecurringPaymentTokenRepositoryTest.php new file mode 100644 index 00000000..a8a91d30 --- /dev/null +++ b/tests/Unit/Repository/RecurringPayment/RecurringPaymentTokenRepositoryTest.php @@ -0,0 +1,111 @@ +entityManager = $this->prophesize(EntityManager::class); + $this->recurringPaymentTokenEntityRepository = $this->prophesize(EntityRepository::class); + $this->recurringPaymentTokenRepository = new RecurringPaymentTokenRepository( + $this->entityManager->reveal(), + $this->recurringPaymentTokenEntityRepository->reveal() + ); + } + + /** @test */ + public function it_is_a_recurring_payment_token_repository(): void + { + $this->assertInstanceOf(RecurringPaymentTokenRepositoryInterface::class, $this->recurringPaymentTokenRepository); + } + + /** @test */ + public function it_can_fetch_a_recurring_payment_token_by_customer_id_and_order_number(): void + { + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + + $this->recurringPaymentTokenEntityRepository->findOneBy([ + 'customerId' => $customerId = 'customer-id', + 'orderNumber' => $orderNumber = 'order-number', + ])->willReturn($recurringPaymentToken->reveal()); + + $result = $this->recurringPaymentTokenRepository->fetchByCustomerIdAndOrderNumber($customerId, $orderNumber); + + self::assertEquals($recurringPaymentToken->reveal(), $result); + } + + /** @test */ + public function it_will_throw_an_error_on_missing_recurring_payment_token_by_customer_id_and_order_number(): void + { + $this->recurringPaymentTokenEntityRepository->findOneBy([ + 'customerId' => $customerId = 'customer-id', + 'orderNumber' => $orderNumber = 'order-number', + ])->willReturn(null); + + self::expectException(RecurringPaymentTokenNotFoundException::class); + + $this->recurringPaymentTokenRepository->fetchByCustomerIdAndOrderNumber($customerId, $orderNumber); + } + + /** @test */ + public function it_can_fetch_a_recurring_payment_token_by_psp_reference(): void + { + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + + $this->recurringPaymentTokenEntityRepository->findOneBy([ + 'resultCode' => PaymentResultCode::pending()->resultCode(), + 'pspReference' => $pspReference = 'psp-reference', + ])->willReturn($recurringPaymentToken->reveal()); + + $result = $this->recurringPaymentTokenRepository->fetchPendingByPspReference($pspReference); + + self::assertEquals($recurringPaymentToken->reveal(), $result); + } + + /** @test */ + public function it_will_throw_an_error_on_missing_recurring_payment_token_by_psp_reference(): void + { + $this->recurringPaymentTokenEntityRepository->findOneBy([ + 'resultCode' => PaymentResultCode::pending()->resultCode(), + 'pspReference' => $pspReference = 'psp-reference', + ])->willReturn(null); + + self::expectException(RecurringPaymentTokenNotFoundException::class); + + $this->recurringPaymentTokenRepository->fetchPendingByPspReference($pspReference); + } + + /** @test */ + public function it_can_update_a_recurring_payment_token(): void + { + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + $this->entityManager->persist($recurringPaymentToken->reveal())->shouldBeCalled(); + $this->entityManager->flush($recurringPaymentToken->reveal())->shouldBeCalled(); + + $this->recurringPaymentTokenRepository->update($recurringPaymentToken->reveal()); + } +} diff --git a/tests/Unit/Repository/RecurringPayment/TraceableRecurringPaymentTokenRepositoryTest.php b/tests/Unit/Repository/RecurringPayment/TraceableRecurringPaymentTokenRepositoryTest.php new file mode 100644 index 00000000..95217042 --- /dev/null +++ b/tests/Unit/Repository/RecurringPayment/TraceableRecurringPaymentTokenRepositoryTest.php @@ -0,0 +1,152 @@ +recurringPaymentTokenRepository = $this->prophesize(RecurringPaymentTokenRepositoryInterface::class); + $this->logger = $this->prophesize(LoggerInterface::class); + $this->traceableRecurringPaymentTokenRepository = new TraceableRecurringPaymentTokenRepository( + $this->recurringPaymentTokenRepository->reveal(), + $this->logger->reveal() + ); + } + + /** @test */ + public function it_is_a_recurring_payment_token_repository(): void + { + $this->assertInstanceOf( + RecurringPaymentTokenRepositoryInterface::class, + $this->traceableRecurringPaymentTokenRepository + ); + } + + /** @test */ + public function it_can_fetch_a_recurring_payment_token_by_customer_id_and_order_number(): void + { + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + + $this->recurringPaymentTokenRepository->fetchByCustomerIdAndOrderNumber( + $customerId = 'customer-id', + $orderNumber = 'order-number' + )->willReturn($recurringPaymentToken->reveal()); + + $result = $this->traceableRecurringPaymentTokenRepository->fetchByCustomerIdAndOrderNumber( + $customerId, + $orderNumber + ); + + self::assertEquals($recurringPaymentToken->reveal(), $result); + } + + /** @test */ + public function it_will_throw_an_error_on_missing_recurring_payment_token_by_customer_id_and_order_number(): void + { + $exception = new RecurringPaymentTokenNotFoundException(); + $this->recurringPaymentTokenRepository->fetchByCustomerIdAndOrderNumber( + $customerId = 'customer-id', + $orderNumber = 'order-number' + )->willThrow($exception); + + $this->logger->info($exception->getMessage(), ['exception' => $exception])->shouldBeCalled(); + + self::expectException(RecurringPaymentTokenNotFoundException::class); + + $this->traceableRecurringPaymentTokenRepository->fetchByCustomerIdAndOrderNumber($customerId, $orderNumber); + } + + /** @test */ + public function it_can_fetch_a_recurring_payment_token_by_psp_reference(): void + { + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + + $this->recurringPaymentTokenRepository->fetchPendingByPspReference($pspReference = 'psp-reference') + ->willReturn($recurringPaymentToken->reveal()); + + $result = $this->traceableRecurringPaymentTokenRepository->fetchPendingByPspReference($pspReference); + + self::assertEquals($recurringPaymentToken->reveal(), $result); + } + + /** @test */ + public function it_will_throw_an_error_on_missing_recurring_payment_token_by_psp_reference(): void + { + $exception = new RecurringPaymentTokenNotFoundException(); + $this->recurringPaymentTokenRepository->fetchPendingByPspReference($pspReference = 'psp-reference') + ->willThrow($exception); + + $this->logger->info($exception->getMessage(), ['exception' => $exception])->shouldBeCalled(); + + self::expectException(RecurringPaymentTokenNotFoundException::class); + + $this->traceableRecurringPaymentTokenRepository->fetchPendingByPspReference($pspReference); + } + + /** @test */ + public function it_can_update_a_recurring_payment_token(): void + { + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + $this->recurringPaymentTokenRepository->update($recurringPaymentToken->reveal())->shouldBeCalled(); + + $this->traceableRecurringPaymentTokenRepository->update($recurringPaymentToken->reveal()); + } + + /** @test */ + public function it_can_catch_a_orm_exception_on_updating_a_recurring_payment_token(): void + { + $ormException = new ORMException(); + $token = TokenIdentifier::generate(); + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + $recurringPaymentToken->tokenIdentifier()->willReturn($token); + + $this->recurringPaymentTokenRepository->update($recurringPaymentToken->reveal())->willThrow($ormException); + $this->logger->error($ormException->getMessage(), ['exception' => $ormException]); + + self::expectException(RecurringPaymentTokenNotSavedException::class); + + $this->traceableRecurringPaymentTokenRepository->update($recurringPaymentToken->reveal()); + } + + /** @test */ + public function it_can_catch_a_orm_invalid_argument_exception_on_updating_a_recurring_payment_token(): void + { + $ormException = new ORMInvalidArgumentException(); + $token = TokenIdentifier::generate(); + $recurringPaymentToken = $this->prophesize(RecurringPaymentToken::class); + $recurringPaymentToken->tokenIdentifier()->willReturn($token); + + $this->recurringPaymentTokenRepository->update($recurringPaymentToken->reveal())->willThrow($ormException); + $this->logger->error($ormException->getMessage(), ['exception' => $ormException]); + + self::expectException(RecurringPaymentTokenNotSavedException::class); + + $this->traceableRecurringPaymentTokenRepository->update($recurringPaymentToken->reveal()); + } +} diff --git a/tests/Unit/Session/CustomerNumberProviderTest.php b/tests/Unit/Session/CustomerNumberProviderTest.php new file mode 100644 index 00000000..d47fcb15 --- /dev/null +++ b/tests/Unit/Session/CustomerNumberProviderTest.php @@ -0,0 +1,87 @@ +session = $this->prophesize(Enlight_Components_Session_Namespace::class); + $this->modelManager = $this->prophesize(ModelManager::class); + $this->customerNumberProvider = new CustomerNumberProvider( + $this->session->reveal(), + $this->modelManager->reveal() + ); + } + + /** @test */ + public function it_is_a_customer_number_provider(): void + { + $this->assertInstanceOf(CustomerNumberProviderInterface::class, $this->customerNumberProvider); + } + + /** @test */ + public function it_provides_empty_string_when_no_user_id_in_session(): void + { + $this->session->get('sUserId')->shouldBeCalledOnce()->willReturn(null); + $this->modelManager->getRepository(Customer::class)->shouldNotBeCalled(); + $customerNumber = ($this->customerNumberProvider)(); + + $this->assertEquals('', $customerNumber); + } + + /** @test */ + public function it_provides_empty_string_when_no_customer_returned_from_repository(): void + { + $customerRepository = $this->prophesize(EntityRepository::class); + + $this->session->get('sUserId')->shouldBeCalledOnce()->willReturn($userId = '123'); + $this->modelManager->getRepository(Customer::class) + ->shouldBeCalledOnce() + ->willReturn($customerRepository->reveal()); + $customerRepository->find($userId)->willReturn(null); + + $customerNumber = ($this->customerNumberProvider)(); + $this->assertEquals('', $customerNumber); + } + + /** @test */ + public function it_provides_customer_number(): void + { + $customer = new Customer(); + $customer->setNumber($customerNumber = 'abc'); + + $customerRepository = $this->prophesize(EntityRepository::class); + + $this->session->get('sUserId')->shouldBeCalledOnce()->willReturn($customerNumber); + $this->modelManager->getRepository(Customer::class) + ->shouldBeCalledOnce() + ->willReturn($customerRepository->reveal()); + $customerRepository->find($customerNumber) + ->willReturn($customer); + + $customerNumberExpected = ($this->customerNumberProvider)(); + $this->assertEquals($customerNumberExpected, $customerNumber); + } +}