From f5bb4048cdcebd036b68dba5edd5f6158f058a69 Mon Sep 17 00:00:00 2001 From: David Remer Date: Mon, 31 Aug 2020 13:38:54 +0300 Subject: [PATCH] add api client to main repository --- .github/workflows/php.yml | 2 +- composer.json | 1 - modules.local/ppcp-api-client/.gitignore | 3 + modules.local/ppcp-api-client/README.md | 3 + modules.local/ppcp-api-client/extensions.php | 9 + modules.local/ppcp-api-client/module.php | 10 + modules.local/ppcp-api-client/phpcs.xml.dist | 21 + modules.local/ppcp-api-client/services.php | 270 +++++ .../ppcp-api-client/src/ApiModule.php | 28 + .../src/Authentication/Bearer.php | 13 + .../src/Authentication/ConnectBearer.php | 21 + .../src/Authentication/PayPalBearer.php | 81 ++ .../src/Endpoint/IdentityToken.php | 88 ++ .../src/Endpoint/LoginSeller.php | 143 +++ .../src/Endpoint/OrderEndpoint.php | 418 +++++++ .../src/Endpoint/PartnerReferrals.php | 104 ++ .../src/Endpoint/PaymentTokenEndpoint.php | 149 +++ .../src/Endpoint/PaymentsEndpoint.php | 140 +++ .../src/Endpoint/RequestTrait.php | 26 + .../src/Endpoint/WebhookEndpoint.php | 227 ++++ .../ppcp-api-client/src/Entity/Address.php | 73 ++ .../ppcp-api-client/src/Entity/Amount.php | 45 + .../src/Entity/AmountBreakdown.php | 98 ++ .../src/Entity/ApplicationContext.php | 154 +++ .../src/Entity/Authorization.php | 38 + .../src/Entity/AuthorizationStatus.php | 61 + .../src/Entity/CardAuthenticationResult.php | 71 ++ .../ppcp-api-client/src/Entity/Item.php | 93 ++ .../ppcp-api-client/src/Entity/Money.php | 35 + .../ppcp-api-client/src/Entity/Order.php | 137 +++ .../src/Entity/OrderStatus.php | 54 + .../ppcp-api-client/src/Entity/Patch.php | 57 + .../src/Entity/PatchCollection.php | 33 + .../ppcp-api-client/src/Entity/Payee.php | 48 + .../ppcp-api-client/src/Entity/Payer.php | 100 ++ .../ppcp-api-client/src/Entity/PayerName.php | 39 + .../src/Entity/PayerTaxInfo.php | 51 + .../src/Entity/PaymentMethod.php | 43 + .../src/Entity/PaymentSource.php | 45 + .../src/Entity/PaymentSourceCard.php | 64 + .../src/Entity/PaymentSourceWallet.php | 14 + .../src/Entity/PaymentToken.php | 47 + .../ppcp-api-client/src/Entity/Payments.php | 35 + .../ppcp-api-client/src/Entity/Phone.php | 27 + .../src/Entity/PhoneWithType.php | 42 + .../src/Entity/PurchaseUnit.php | 231 ++++ .../ppcp-api-client/src/Entity/Shipping.php | 37 + .../ppcp-api-client/src/Entity/Token.php | 72 ++ .../ppcp-api-client/src/Entity/Webhook.php | 50 + .../src/Exception/NotFoundException.php | 13 + .../src/Exception/PayPalApiException.php | 66 ++ .../src/Exception/RuntimeException.php | 10 + .../src/Factory/AddressFactory.php | 63 + .../src/Factory/AmountFactory.php | 169 +++ .../src/Factory/ApplicationContextFactory.php | 31 + .../src/Factory/AuthorizationFactory.php | 32 + .../src/Factory/ItemFactory.php | 124 ++ .../src/Factory/OrderFactory.php | 113 ++ .../src/Factory/PatchCollectionFactory.php | 66 ++ .../src/Factory/PayeeFactory.php | 24 + .../src/Factory/PayerFactory.php | 82 ++ .../src/Factory/PaymentSourceFactory.php | 40 + .../src/Factory/PaymentTokenFactory.php | 30 + .../src/Factory/PaymentsFactory.php | 32 + .../src/Factory/PurchaseUnitFactory.php | 166 +++ .../src/Factory/ShippingFactory.php | 63 + .../src/Factory/WebhookFactory.php | 56 + .../ppcp-api-client/src/Helper/DccApplies.php | 136 +++ .../src/Helper/ErrorResponse.php | 114 ++ .../ApplicationContextRepository.php | 39 + .../src/Repository/CartRepository.php | 29 + .../src/Repository/PartnerReferralsData.php | 81 ++ .../Repository/PayPalRequestIdRepository.php | 54 + .../src/Repository/PayeeRepository.php | 28 + .../PurchaseUnitRepositoryInterface.php | 16 + .../Authentication/PayPalBearerTest.php | 220 ++++ .../ApiClient/Endpoint/OrderEndpointTest.php | 1051 +++++++++++++++++ .../PHPUnit/ApiClient/Endpoint/OrderTest.php | 100 ++ .../Endpoint/PaymentsEndpointTest.php | 259 ++++ .../PHPUnit/ApiClient/Entity/AddressTest.php | 53 + .../ApiClient/Entity/AmountBreakdownTest.php | 123 ++ tests/PHPUnit/ApiClient/Entity/AmountTest.php | 57 + .../Entity/AuthorizationStatusTest.php | 51 + .../ApiClient/Entity/AuthorizationTest.php | 33 + tests/PHPUnit/ApiClient/Entity/ItemTest.php | 84 ++ tests/PHPUnit/ApiClient/Entity/MoneyTest.php | 23 + tests/PHPUnit/ApiClient/Entity/PayerTest.php | 202 ++++ .../PHPUnit/ApiClient/Entity/PaymentsTest.php | 46 + .../ApiClient/Entity/PurchaseUnitTest.php | 502 ++++++++ tests/PHPUnit/ApiClient/Entity/TokenTest.php | 137 +++ .../ApiClient/Factory/AddressFactoryTest.php | 205 ++++ .../ApiClient/Factory/AmountFactoryTest.php | 581 +++++++++ .../Factory/AuthorizationFactoryTest.php | 53 + .../ApiClient/Factory/ItemFactoryTest.php | 420 +++++++ .../ApiClient/Factory/OrderFactoryTest.php | 221 ++++ .../ApiClient/Factory/PayerFactoryTest.php | 269 +++++ .../ApiClient/Factory/PaymentsFactoryTest.php | 40 + .../Factory/PurchaseUnitFactoryTest.php | 659 +++++++++++ .../Repository/PayeeRepositoryTest.php | 22 + tests/PHPUnit/ApiClient/TestCase.php | 28 + 100 files changed, 10765 insertions(+), 2 deletions(-) create mode 100644 modules.local/ppcp-api-client/.gitignore create mode 100644 modules.local/ppcp-api-client/README.md create mode 100644 modules.local/ppcp-api-client/extensions.php create mode 100644 modules.local/ppcp-api-client/module.php create mode 100644 modules.local/ppcp-api-client/phpcs.xml.dist create mode 100644 modules.local/ppcp-api-client/services.php create mode 100644 modules.local/ppcp-api-client/src/ApiModule.php create mode 100644 modules.local/ppcp-api-client/src/Authentication/Bearer.php create mode 100644 modules.local/ppcp-api-client/src/Authentication/ConnectBearer.php create mode 100644 modules.local/ppcp-api-client/src/Authentication/PayPalBearer.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/IdentityToken.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/LoginSeller.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/OrderEndpoint.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/PartnerReferrals.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/RequestTrait.php create mode 100644 modules.local/ppcp-api-client/src/Endpoint/WebhookEndpoint.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Address.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Amount.php create mode 100644 modules.local/ppcp-api-client/src/Entity/AmountBreakdown.php create mode 100644 modules.local/ppcp-api-client/src/Entity/ApplicationContext.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Authorization.php create mode 100644 modules.local/ppcp-api-client/src/Entity/AuthorizationStatus.php create mode 100644 modules.local/ppcp-api-client/src/Entity/CardAuthenticationResult.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Item.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Money.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Order.php create mode 100644 modules.local/ppcp-api-client/src/Entity/OrderStatus.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Patch.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PatchCollection.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Payee.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Payer.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PayerName.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PayerTaxInfo.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PaymentMethod.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PaymentSource.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PaymentSourceCard.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PaymentSourceWallet.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PaymentToken.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Payments.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Phone.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PhoneWithType.php create mode 100644 modules.local/ppcp-api-client/src/Entity/PurchaseUnit.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Shipping.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Token.php create mode 100644 modules.local/ppcp-api-client/src/Entity/Webhook.php create mode 100644 modules.local/ppcp-api-client/src/Exception/NotFoundException.php create mode 100644 modules.local/ppcp-api-client/src/Exception/PayPalApiException.php create mode 100644 modules.local/ppcp-api-client/src/Exception/RuntimeException.php create mode 100644 modules.local/ppcp-api-client/src/Factory/AddressFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/AmountFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/ApplicationContextFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/AuthorizationFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/ItemFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/OrderFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/PatchCollectionFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/PayeeFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/PayerFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/PaymentSourceFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/PaymentTokenFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/PaymentsFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/PurchaseUnitFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/ShippingFactory.php create mode 100644 modules.local/ppcp-api-client/src/Factory/WebhookFactory.php create mode 100644 modules.local/ppcp-api-client/src/Helper/DccApplies.php create mode 100644 modules.local/ppcp-api-client/src/Helper/ErrorResponse.php create mode 100644 modules.local/ppcp-api-client/src/Repository/ApplicationContextRepository.php create mode 100644 modules.local/ppcp-api-client/src/Repository/CartRepository.php create mode 100644 modules.local/ppcp-api-client/src/Repository/PartnerReferralsData.php create mode 100644 modules.local/ppcp-api-client/src/Repository/PayPalRequestIdRepository.php create mode 100644 modules.local/ppcp-api-client/src/Repository/PayeeRepository.php create mode 100644 modules.local/ppcp-api-client/src/Repository/PurchaseUnitRepositoryInterface.php create mode 100644 tests/PHPUnit/ApiClient/Authentication/PayPalBearerTest.php create mode 100644 tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php create mode 100644 tests/PHPUnit/ApiClient/Endpoint/OrderTest.php create mode 100644 tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/AddressTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/AmountBreakdownTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/AmountTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/AuthorizationStatusTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/ItemTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/MoneyTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/PayerTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/PaymentsTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php create mode 100644 tests/PHPUnit/ApiClient/Entity/TokenTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/AddressFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/AmountFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/PayerFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php create mode 100644 tests/PHPUnit/ApiClient/Repository/PayeeRepositoryTest.php create mode 100644 tests/PHPUnit/ApiClient/TestCase.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index e847252a0..d0da74d3f 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -29,4 +29,4 @@ jobs: - name: Run test suite run: ./vendor/bin/phpunit - name: Run cs - run: ./vendor/bin/phpcs src inc modules.local/ppcp-wc-gateway/ modules.local/ppcp-webhooks/ modules.local/ppcp-button/ modules.local/ppcp-onboarding/ modules.local/ppcp-subscription --extensions=php + run: ./vendor/bin/phpcs src inc modules.local --extensions=php diff --git a/composer.json b/composer.json index cd6215e59..e1d3b9194 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "dhii/module-interface": "0.2.x-dev", "psr/container": "^1.0", "inpsyde/ppcp-admin-notices": "dev-master", - "inpsyde/ppcp-api-client": "dev-master", "inpsyde/ppcp-session": "dev-master", "oomphinc/composer-installers-extender": "^1.1", "container-interop/service-provider": "^0.4.0", diff --git a/modules.local/ppcp-api-client/.gitignore b/modules.local/ppcp-api-client/.gitignore new file mode 100644 index 000000000..a436d2967 --- /dev/null +++ b/modules.local/ppcp-api-client/.gitignore @@ -0,0 +1,3 @@ +vendor/ +build/ +composer.lock \ No newline at end of file diff --git a/modules.local/ppcp-api-client/README.md b/modules.local/ppcp-api-client/README.md new file mode 100644 index 000000000..03f40af44 --- /dev/null +++ b/modules.local/ppcp-api-client/README.md @@ -0,0 +1,3 @@ +# PPCP API Client + +This module takes care of the api client for the woocommerce-paypal-commerce-gateway. \ No newline at end of file diff --git a/modules.local/ppcp-api-client/extensions.php b/modules.local/ppcp-api-client/extensions.php new file mode 100644 index 000000000..57ca3b7c1 --- /dev/null +++ b/modules.local/ppcp-api-client/extensions.php @@ -0,0 +1,9 @@ + + + My projects ruleset. + + + + + + + + + + + + + + + + tests/ + + diff --git a/modules.local/ppcp-api-client/services.php b/modules.local/ppcp-api-client/services.php new file mode 100644 index 000000000..bea7b45f4 --- /dev/null +++ b/modules.local/ppcp-api-client/services.php @@ -0,0 +1,270 @@ + function(ContainerInterface $container) : string { + return 'https://api.paypal.com'; + }, + 'api.paypal-host' => function(ContainerInterface $container) : string { + return 'https://api.paypal.com'; + }, + 'api.partner_merchant_id' => static function () : string { + return ''; + }, + 'api.merchant_email' => function () : string { + return ''; + }, + 'api.merchant_id' => function () : string { + return ''; + }, + 'api.key' => static function (): string { + return ''; + }, + 'api.secret' => static function (): string { + return ''; + }, + 'api.prefix' => static function (): string { + return 'WC-'; + }, + 'api.bearer' => static function (ContainerInterface $container): Bearer { + global $wpdb; + $cacheFactory = new CachePoolFactory($wpdb); + $pool = $cacheFactory->createCachePool('ppcp-token'); + $key = $container->get('api.key'); + $secret = $container->get('api.secret'); + + $host = $container->get('api.host'); + $logger = $container->get('woocommerce.logger.woocommerce'); + return new PayPalBearer( + $pool, + $host, + $key, + $secret, + $logger + ); + }, + 'api.endpoint.payment-token' => static function (ContainerInterface $container) : PaymentTokenEndpoint { + return new PaymentTokenEndpoint( + $container->get('api.host'), + $container->get('api.bearer'), + $container->get('api.factory.payment-token'), + $container->get('woocommerce.logger.woocommerce'), + $container->get('api.prefix') + ); + }, + 'api.endpoint.webhook' => static function (ContainerInterface $container) : WebhookEndpoint { + + return new WebhookEndpoint( + $container->get('api.host'), + $container->get('api.bearer'), + $container->get('api.factory.webhook'), + $container->get('woocommerce.logger.woocommerce') + ); + }, + 'api.endpoint.partner-referrals' => static function (ContainerInterface $container) : PartnerReferrals { + + return new PartnerReferrals( + $container->get('api.host'), + $container->get('api.bearer'), + $container->get('api.repository.partner-referrals-data'), + $container->get('woocommerce.logger.woocommerce') + ); + }, + 'api.endpoint.identity-token' => static function (ContainerInterface $container) : IdentityToken { + + $logger = $container->get('woocommerce.logger.woocommerce'); + $prefix = $container->get('api.prefix'); + return new IdentityToken( + $container->get('api.host'), + $container->get('api.bearer'), + $logger, + $prefix + ); + }, + 'api.endpoint.payments' => static function (ContainerInterface $container): PaymentsEndpoint { + $authorizationFactory = $container->get('api.factory.authorization'); + $logger = $container->get('woocommerce.logger.woocommerce'); + + return new PaymentsEndpoint( + $container->get('api.host'), + $container->get('api.bearer'), + $authorizationFactory, + $logger + ); + }, + 'api.endpoint.login-seller' => static function (ContainerInterface $container) : LoginSeller { + + $logger = $container->get('woocommerce.logger.woocommerce'); + return new LoginSeller( + + $container->get('api.paypal-host'), + $container->get('api.partner_merchant_id'), + $logger + ); + }, + 'api.endpoint.order' => static function (ContainerInterface $container): OrderEndpoint { + $orderFactory = $container->get('api.factory.order'); + $patchCollectionFactory = $container->get('api.factory.patch-collection-factory'); + $logger = $container->get('woocommerce.logger.woocommerce'); + + /** + * @var Settings $settings + */ + $settings = $container->get('wcgateway.settings'); + $intent = $settings->has('intent') && strtoupper((string) $settings->get('intent')) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE'; + $applicationContextRepository = $container->get('api.repository.application-context'); + $paypalRequestId = $container->get('api.repository.paypal-request-id'); + return new OrderEndpoint( + $container->get('api.host'), + $container->get('api.bearer'), + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestId + ); + }, + 'api.repository.paypal-request-id' => static function(ContainerInterface $container) : PayPalRequestIdRepository { + return new PayPalRequestIdRepository(); + }, + 'api.repository.application-context' => static function(ContainerInterface $container) : ApplicationContextRepository { + + $settings = $container->get('wcgateway.settings'); + return new ApplicationContextRepository($settings); + }, + 'api.repository.partner-referrals-data' => static function (ContainerInterface $container) : PartnerReferralsData { + + $merchantEmail = $container->get('api.merchant_email'); + $dccApplies = $container->get('api.helpers.dccapplies'); + return new PartnerReferralsData($merchantEmail, $dccApplies); + }, + 'api.repository.cart' => static function (ContainerInterface $container): CartRepository { + $factory = $container->get('api.factory.purchase-unit'); + return new CartRepository($factory); + }, + 'api.repository.payee' => static function (ContainerInterface $container): PayeeRepository { + $merchantEmail = $container->get('api.merchant_email'); + $merchantId = $container->get('api.merchant_id'); + return new PayeeRepository($merchantEmail, $merchantId); + }, + 'api.factory.application-context' => static function (ContainerInterface $container) : ApplicationContextFactory { + return new ApplicationContextFactory(); + }, + 'api.factory.payment-token' => static function (ContainerInterface $container) : PaymentTokenFactory { + return new PaymentTokenFactory(); + }, + 'api.factory.webhook' => static function (ContainerInterface $container): WebhookFactory { + return new WebhookFactory(); + }, + 'api.factory.purchase-unit' => static function (ContainerInterface $container): PurchaseUnitFactory { + + $amountFactory = $container->get('api.factory.amount'); + $payeeRepository = $container->get('api.repository.payee'); + $payeeFactory = $container->get('api.factory.payee'); + $itemFactory = $container->get('api.factory.item'); + $shippingFactory = $container->get('api.factory.shipping'); + $paymentsFactory = $container->get('api.factory.payments'); + $prefix = $container->get('api.prefix'); + + return new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFactory, + $prefix + ); + }, + 'api.factory.patch-collection-factory' => static function (ContainerInterface $container): PatchCollectionFactory { + return new PatchCollectionFactory(); + }, + 'api.factory.payee' => static function (ContainerInterface $container): PayeeFactory { + return new PayeeFactory(); + }, + 'api.factory.item' => static function (ContainerInterface $container): ItemFactory { + return new ItemFactory(); + }, + 'api.factory.shipping' => static function (ContainerInterface $container): ShippingFactory { + $addressFactory = $container->get('api.factory.address'); + return new ShippingFactory($addressFactory); + }, + 'api.factory.amount' => static function (ContainerInterface $container): AmountFactory { + $itemFactory = $container->get('api.factory.item'); + return new AmountFactory($itemFactory); + }, + 'api.factory.payer' => static function (ContainerInterface $container): PayerFactory { + $addressFactory = $container->get('api.factory.address'); + return new PayerFactory($addressFactory); + }, + 'api.factory.address' => static function (ContainerInterface $container): AddressFactory { + return new AddressFactory(); + }, + 'api.factory.payment-source' => static function (ContainerInterface $container): PaymentSourceFactory { + return new PaymentSourceFactory(); + }, + 'api.factory.order' => static function (ContainerInterface $container): OrderFactory { + $purchaseUnitFactory = $container->get('api.factory.purchase-unit'); + $payerFactory = $container->get('api.factory.payer'); + $applicationContextRepository = $container->get('api.repository.application-context'); + $applicationContextFactory = $container->get('api.factory.application-context'); + $paymentSourceFactory = $container->get('api.factory.payment-source'); + return new OrderFactory( + $purchaseUnitFactory, + $payerFactory, + $applicationContextRepository, + $applicationContextFactory, + $paymentSourceFactory + ); + }, + 'api.factory.payments' => static function (ContainerInterface $container): PaymentsFactory { + $authorizationFactory = $container->get('api.factory.authorization'); + return new PaymentsFactory($authorizationFactory); + }, + 'api.factory.authorization' => static function (ContainerInterface $container): AuthorizationFactory { + return new AuthorizationFactory(); + }, + 'api.helpers.dccapplies' => static function (ContainerInterface $container) : DccApplies { + return new DccApplies(); + }, +]; diff --git a/modules.local/ppcp-api-client/src/ApiModule.php b/modules.local/ppcp-api-client/src/ApiModule.php new file mode 100644 index 000000000..10bb535d2 --- /dev/null +++ b/modules.local/ppcp-api-client/src/ApiModule.php @@ -0,0 +1,28 @@ + time(), + 'expires_in' => 3600, + 'token' => 'token', + ]; + return new Token($data); + } +} diff --git a/modules.local/ppcp-api-client/src/Authentication/PayPalBearer.php b/modules.local/ppcp-api-client/src/Authentication/PayPalBearer.php new file mode 100644 index 000000000..b06a23232 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Authentication/PayPalBearer.php @@ -0,0 +1,81 @@ +cache = $cache; + $this->host = $host; + $this->key = $key; + $this->secret = $secret; + $this->logger = $logger; + } + + public function bearer(): Token + { + try { + $bearer = Token::fromJson((string) $this->cache->get(self::CACHE_KEY)); + return ($bearer->isValid()) ? $bearer : $this->newBearer(); + } catch (RuntimeException $error) { + return $this->newBearer(); + } + } + + private function newBearer(): Token + { + $url = trailingslashit($this->host) . 'v1/oauth2/token?grant_type=client_credentials'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode($this->key . ':' . $this->secret), + ], + ]; + $response = $this->request( + $url, + $args + ); + + if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { + $error = new RuntimeException( + __('Could not create token.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $token = Token::fromJson($response['body']); + $this->cache->set(self::CACHE_KEY, $token->asJson()); + return $token; + } +} diff --git a/modules.local/ppcp-api-client/src/Endpoint/IdentityToken.php b/modules.local/ppcp-api-client/src/Endpoint/IdentityToken.php new file mode 100644 index 000000000..ed5ca4975 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Endpoint/IdentityToken.php @@ -0,0 +1,88 @@ +host = $host; + $this->bearer = $bearer; + $this->logger = $logger; + $this->prefix = $prefix; + } + + public function generateForCustomer(int $customerId): Token + { + + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v1/identity/generate-token'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ], + ]; + if ($customerId && defined('PPCP_FLAG_SUBSCRIPTION') && PPCP_FLAG_SUBSCRIPTION) { + $args['body'] = json_encode(['customer_id' => $this->prefix . $customerId]); + } + + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __( + 'Could not create identity token.', + 'woocommerce-paypal-commerce-gateway' + ) + ); + + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 200) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $token = Token::fromJson($response['body']); + return $token; + } +} diff --git a/modules.local/ppcp-api-client/src/Endpoint/LoginSeller.php b/modules.local/ppcp-api-client/src/Endpoint/LoginSeller.php new file mode 100644 index 000000000..7429992a2 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Endpoint/LoginSeller.php @@ -0,0 +1,143 @@ +host = $host; + $this->partnerMerchantId = $partnerMerchantId; + $this->logger = $logger; + } + + public function credentialsFor( + string $sharedId, + string $authCode, + string $sellerNonce + ): \stdClass { + + $token = $this->generateTokenFor($sharedId, $authCode, $sellerNonce); + $url = trailingslashit($this->host) . + 'v1/customer/partners/' . $this->partnerMerchantId . + '/merchant-integrations/credentials/'; + $args = [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ], + ]; + $response = $this->request($url, $args); + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not fetch credentials.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if (! isset($json->client_id) || ! isset($json->client_secret)) { + $error = isset($json->details) ? + new PayPalApiException( + $json, + $statusCode + ) : new RuntimeException( + __('Credentials not found.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + return $json; + } + + private function generateTokenFor( + string $sharedId, + string $authCode, + string $sellerNonce + ): string { + + $url = trailingslashit($this->host) . 'v1/oauth2/token/'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode($sharedId . ':'), + ], + 'body' => [ + 'grant_type' => 'authorization_code', + 'code' => $authCode, + 'code_verifier' => $sellerNonce, + ], + ]; + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not create token.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if (! isset($json->access_token)) { + $error = isset($json->details) ? + new PayPalApiException( + $json, + $statusCode + ) : new RuntimeException( + __('No token found.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + return (string) $json->access_token; + } +} diff --git a/modules.local/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules.local/ppcp-api-client/src/Endpoint/OrderEndpoint.php new file mode 100644 index 000000000..3c0dd65df --- /dev/null +++ b/modules.local/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -0,0 +1,418 @@ +host = $host; + $this->bearer = $bearer; + $this->orderFactory = $orderFactory; + $this->patchCollectionFactory = $patchCollectionFactory; + $this->intent = $intent; + $this->logger = $logger; + $this->applicationContextRepository = $applicationContextRepository; + $this->bnCode = $bnCode; + $this->payPalRequestIdRepository = $payPalRequestIdRepository; + } + + public function withBnCode(string $bnCode): OrderEndpoint + { + + $this->bnCode = $bnCode; + return $this; + } + + /** + * @param PurchaseUnit[] $items + */ + public function createForPurchaseUnits( + array $items, + Payer $payer = null, + PaymentToken $paymentToken = null, + PaymentMethod $paymentMethod = null, + string $paypalRequestId = '' + ): Order { + + $containsPhysicalGoods = false; + $items = array_filter( + $items, + //phpcs:ignore Inpsyde.CodeQuality.ArgumentTypeDeclaration.NoArgumentType + static function ($item) use (&$containsPhysicalGoods): bool { + $isPurchaseUnit = is_a($item, PurchaseUnit::class); + if ($isPurchaseUnit && $item->containsPhysicalGoodsItems()) { + $containsPhysicalGoods = true; + } + + return $isPurchaseUnit; + } + ); + $shippingPreference = $containsPhysicalGoods + ? ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE + : ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; + $bearer = $this->bearer->bearer(); + $data = [ + 'intent' => $this->intent, + 'purchase_units' => array_map( + static function (PurchaseUnit $item): array { + return $item->toArray(); + }, + $items + ), + 'application_context' => $this->applicationContextRepository + ->currentContext($shippingPreference)->toArray(), + ]; + if ($payer) { + $data['payer'] = $payer->toArray(); + } + if ($paymentToken) { + $data['payment_source']['token'] = $paymentToken->toArray(); + } + if ($paymentMethod) { + $data['payment_method'] = $paymentMethod->toArray(); + } + $url = trailingslashit($this->host) . 'v2/checkout/orders'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ], + 'body' => json_encode($data), + ]; + + $paypalRequestId = $paypalRequestId ? $paypalRequestId : uniqid('ppcp-', true); + $args['headers']['PayPal-Request-Id'] = $paypalRequestId; + if ($this->bnCode) { + $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bnCode; + } + $response = $this->request($url, $args); + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not create order.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 201) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $order = $this->orderFactory->fromPayPalResponse($json); + $this->payPalRequestIdRepository->setForOrder($order, $paypalRequestId); + return $order; + } + + public function capture(Order $order): Order + { + if ($order->status()->is(OrderStatus::COMPLETED)) { + return $order; + } + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v2/checkout/orders/' . $order->id() . '/capture'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + 'PayPal-Request-Id' => $this->payPalRequestIdRepository->getForOrder($order), + ], + ]; + if ($this->bnCode) { + $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bnCode; + } + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not capture order.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 201) { + $error = new PayPalApiException( + $json, + $statusCode + ); + // If the order has already been captured, we return the updated order. + if (strpos($response['body'], ErrorResponse::ORDER_ALREADY_CAPTURED) !== false) { + return $this->order($order->id()); + } + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $order = $this->orderFactory->fromPayPalResponse($json); + return $order; + } + + public function authorize(Order $order): Order + { + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v2/checkout/orders/' . $order->id() . '/authorize'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + 'PayPal-Request-Id' => $this->payPalRequestIdRepository->getForOrder($order), + ], + ]; + if ($this->bnCode) { + $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bnCode; + } + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __( + 'Could not authorize order.', + 'woocommerce-paypal-commerce-gateway' + ) + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 201) { + if (strpos($response['body'], ErrorResponse::ORDER_ALREADY_AUTHORIZED) !== false) { + return $this->order($order->id()); + } + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $order = $this->orderFactory->fromPayPalResponse($json); + return $order; + } + + public function order(string $id): Order + { + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v2/checkout/orders/' . $id; + $args = [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'PayPal-Request-Id' => $this->payPalRequestIdRepository->getForOrderId($id), + ], + ]; + if ($this->bnCode) { + $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bnCode; + } + $response = $this->request($url, $args); + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not retrieve order.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode === 404 || empty($response['body'])) { + $error = new RuntimeException( + __('Could not retrieve order.', 'woocommerce-paypal-commerce-gateway'), + 404 + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + if ($statusCode !== 200) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $order = $this->orderFactory->fromPayPalResponse($json); + return $order; + } + + public function patchOrderWith(Order $orderToUpdate, Order $orderToCompare): Order + { + $patches = $this->patchCollectionFactory->fromOrders($orderToUpdate, $orderToCompare); + if (! count($patches->patches())) { + return $orderToUpdate; + } + + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v2/checkout/orders/' . $orderToUpdate->id(); + $args = [ + 'method' => 'PATCH', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + 'PayPal-Request-Id' => $this->payPalRequestIdRepository->getForOrder( + $orderToUpdate + ), + ], + 'body' => json_encode($patches->toArray()), + ]; + if ($this->bnCode) { + $args['headers']['PayPal-Partner-Attribution-Id'] = $this->bnCode; + } + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not retrieve order.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 204) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $newOrder = $this->order($orderToUpdate->id()); + return $newOrder; + } +} diff --git a/modules.local/ppcp-api-client/src/Endpoint/PartnerReferrals.php b/modules.local/ppcp-api-client/src/Endpoint/PartnerReferrals.php new file mode 100644 index 000000000..a9bbed3af --- /dev/null +++ b/modules.local/ppcp-api-client/src/Endpoint/PartnerReferrals.php @@ -0,0 +1,104 @@ +host = $host; + $this->bearer = $bearer; + $this->data = $data; + $this->logger = $logger; + } + + public function signupLink(): string + { + $data = $this->data->data(); + $bearer = $this->bearer->bearer(); + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ], + 'body' => json_encode($data), + ]; + $url = trailingslashit($this->host) . 'v2/customer/partner-referrals'; + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not create referral.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 201) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + foreach ($json->links as $link) { + if ($link->rel === 'action_url') { + return (string) $link->href; + } + } + + $error = new RuntimeException( + __('Action URL not found.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } +} diff --git a/modules.local/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php b/modules.local/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php new file mode 100644 index 000000000..3891fc284 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Endpoint/PaymentTokenEndpoint.php @@ -0,0 +1,149 @@ +host = $host; + $this->bearer = $bearer; + $this->factory = $factory; + $this->logger = $logger; + $this->prefix = $prefix; + } + + // phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong + /** + * @param int $id + * @return PaymentToken[] + */ + public function forUser(int $id): array + { + $bearer = $this->bearer->bearer(); + + $customerId = $this->prefix . $id; + $url = trailingslashit($this->host) . 'v2/vault/payment-tokens/?customer_id=' . $customerId; + $args = [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ], + ]; + + $response = $this->request($url, $args); + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not fetch payment token.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 200) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $tokens = []; + foreach ($json->payment_tokens as $tokenValue) { + $tokens[] = $this->factory->fromPayPalResponse($tokenValue); + } + if (empty($tokens)) { + $error = new RuntimeException( + sprintf( + // translators: %d is the customer id. + __('No token stored for customer %d.', 'woocommerce-paypal-commerce-gateway'), + $id + ) + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + return $tokens; + } + // phpcs:enable Inpsyde.CodeQuality.FunctionLength.TooLong + + public function deleteToken(PaymentToken $token): bool + { + + $bearer = $this->bearer->bearer(); + + $url = trailingslashit($this->host) . 'v2/vault/payment-tokens/' . $token->id(); + $args = [ + 'method' => 'DELETE', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ], + ]; + + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not delete payment token.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + return wp_remote_retrieve_response_code($response) === 204; + } +} diff --git a/modules.local/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php b/modules.local/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php new file mode 100644 index 000000000..62086daff --- /dev/null +++ b/modules.local/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php @@ -0,0 +1,140 @@ +host = $host; + $this->bearer = $bearer; + $this->authorizationFactory = $authorizationsFactory; + $this->logger = $logger; + } + + public function authorization(string $authorizationId): Authorization + { + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v2/payments/authorizations/' . $authorizationId; + $args = [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ], + ]; + + $response = $this->request($url, $args); + $json = json_decode($response['body']); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not get authorized payment info.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 200) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $authorization = $this->authorizationFactory->fromPayPalRequest($json); + return $authorization; + } + + public function capture(string $authorizationId): Authorization + { + $bearer = $this->bearer->bearer(); + //phpcs:ignore Inpsyde.CodeQuality.LineLength.TooLong + $url = trailingslashit($this->host) . 'v2/payments/authorizations/' . $authorizationId . '/capture'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ], + ]; + + $response = $this->request($url, $args); + $json = json_decode($response['body']); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Could not capture authorized payment.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 201) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $authorization = $this->authorizationFactory->fromPayPalRequest($json); + return $authorization; + } +} diff --git a/modules.local/ppcp-api-client/src/Endpoint/RequestTrait.php b/modules.local/ppcp-api-client/src/Endpoint/RequestTrait.php new file mode 100644 index 000000000..478ccbf61 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Endpoint/RequestTrait.php @@ -0,0 +1,26 @@ +host = $host; + $this->bearer = $bearer; + $this->webhookFactory = $webhookFactory; + $this->logger = $logger; + } + + public function create(Webhook $hook): Webhook + { + /** + * An hook, which has an ID has already been created. + */ + if ($hook->id()) { + return $hook; + } + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v1/notifications/webhooks'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ], + 'body' => json_encode($hook->toArray()), + ]; + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Not able to create a webhook.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $json = json_decode($response['body']); + $statusCode = (int) wp_remote_retrieve_response_code($response); + if ($statusCode !== 201) { + $error = new PayPalApiException( + $json, + $statusCode + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + + $hook = $this->webhookFactory->fromPayPalResponse($json); + return $hook; + } + + public function delete(Webhook $hook): bool + { + if (! $hook->id()) { + return false; + } + + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v1/notifications/webhooks/' . $hook->id(); + $args = [ + 'method' => 'DELETE', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + ], + ]; + $response = $this->request($url, $args); + + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Not able to delete the webhook.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + [ + 'args' => $args, + 'response' => $response, + ] + ); + throw $error; + } + return wp_remote_retrieve_response_code($response) === 204; + } + + public function verifyEvent( + string $authAlgo, + string $certUrl, + string $transmissionId, + string $transmissionSig, + string $transmissionTime, + string $webhookId, + \stdClass $webhookEvent + ): bool { + + $bearer = $this->bearer->bearer(); + $url = trailingslashit($this->host) . 'v1/notifications/verify-webhook-signature'; + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ], + 'body' => json_encode( + [ + 'transmission_id' => $transmissionId, + 'transmission_time' => $transmissionTime, + 'cert_url' => $certUrl, + 'auth_algo' => $authAlgo, + 'transmission_sig' => $transmissionSig, + 'webhook_id' => $webhookId, + 'webhook_event' => $webhookEvent, + ] + ), + ]; + $response = $this->request($url, $args); + if (is_wp_error($response)) { + $error = new RuntimeException( + __('Not able to verify webhook event.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log( + 'warning', + $error->getMessage(), + ['args' => $args, 'response' => $response] + ); + throw $error; + } + $json = json_decode($response['body']); + return isset($json->verification_status) && $json->verification_status === "SUCCESS"; + } + + public function verifyCurrentRequestForWebhook(Webhook $webhook): bool + { + + if (! $webhook->id()) { + $error = new RuntimeException( + __('Not a valid webhook to verify.', 'woocommerce-paypal-commerce-gateway') + ); + $this->logger->log('warning', $error->getMessage(), ['webhook' => $webhook]); + throw $error; + } + + $expectedHeaders = [ + 'PAYPAL-AUTH-ALGO' => '', + 'PAYPAL-CERT-URL' => '', + 'PAYPAL-TRANSMISSION-ID' => '', + 'PAYPAL-TRANSMISSION-SIG' => '', + 'PAYPAL-TRANSMISSION-TIME' => '', + ]; + $headers = getallheaders(); + foreach ($headers as $key => $header) { + $key = strtoupper($key); + if (isset($expectedHeaders[$key])) { + $expectedHeaders[$key] = $header; + } + }; + + foreach ($expectedHeaders as $key => $value) { + if (! empty($value)) { + continue; + } + + $error = new RuntimeException( + sprintf( + // translators: %s is the headers key. + __( + 'Not a valid webhook event. Header %s is missing', + 'woocommerce-paypal-commerce-gateway' + ), + $key + ) + ); + $this->logger->log('warning', $error->getMessage(), ['webhook' => $webhook]); + throw $error; + } + + $requestBody = json_decode(file_get_contents("php://input")); + return $this->verifyEvent( + $expectedHeaders['PAYPAL-AUTH-ALGO'], + $expectedHeaders['PAYPAL-CERT-URL'], + $expectedHeaders['PAYPAL-TRANSMISSION-ID'], + $expectedHeaders['PAYPAL-TRANSMISSION-SIG'], + $expectedHeaders['PAYPAL-TRANSMISSION-TIME'], + $webhook->id(), + $requestBody ? $requestBody : new \stdClass() + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Address.php b/modules.local/ppcp-api-client/src/Entity/Address.php new file mode 100644 index 000000000..3e87ee9de --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Address.php @@ -0,0 +1,73 @@ +countryCode = $countryCode; + $this->addressLine1 = $addressLine1; + $this->addressLine2 = $addressLine2; + $this->adminArea1 = $adminArea1; + $this->adminArea2 = $adminArea2; + $this->postalCode = $postalCode; + } + + public function countryCode(): string + { + return $this->countryCode; + } + + public function addressLine1(): string + { + return $this->addressLine1; + } + + public function addressLine2(): string + { + return $this->addressLine2; + } + + public function adminArea1(): string + { + return $this->adminArea1; + } + + public function adminArea2(): string + { + return $this->adminArea2; + } + + public function postalCode(): string + { + return $this->postalCode; + } + + public function toArray(): array + { + return [ + 'country_code' => $this->countryCode(), + 'address_line_1' => $this->addressLine1(), + 'address_line_2' => $this->addressLine2(), + 'admin_area_1' => $this->adminArea1(), + 'admin_area_2' => $this->adminArea2(), + 'postal_code' => $this->postalCode(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Amount.php b/modules.local/ppcp-api-client/src/Entity/Amount.php new file mode 100644 index 000000000..fb2acd2b7 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Amount.php @@ -0,0 +1,45 @@ +money = $money; + $this->breakdown = $breakdown; + } + + public function currencyCode(): string + { + return $this->money->currencyCode(); + } + + public function value(): float + { + return $this->money->value(); + } + + public function breakdown(): ?AmountBreakdown + { + return $this->breakdown; + } + + public function toArray(): array + { + $amount = [ + 'currency_code' => $this->currencyCode(), + 'value' => $this->value(), + ]; + if ($this->breakdown() && count($this->breakdown()->toArray())) { + $amount['breakdown'] = $this->breakdown()->toArray(); + } + return $amount; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/AmountBreakdown.php b/modules.local/ppcp-api-client/src/Entity/AmountBreakdown.php new file mode 100644 index 000000000..4cbd3d2c4 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/AmountBreakdown.php @@ -0,0 +1,98 @@ +itemTotal = $itemTotal; + $this->shipping = $shipping; + $this->taxTotal = $taxTotal; + $this->handling = $handling; + $this->insurance = $insurance; + $this->shippingDiscount = $shippingDiscount; + $this->discount = $discount; + } + + public function itemTotal(): ?Money + { + return $this->itemTotal; + } + + public function shipping(): ?Money + { + return $this->shipping; + } + + public function taxTotal(): ?Money + { + return $this->taxTotal; + } + + public function handling(): ?Money + { + return $this->handling; + } + + public function insurance(): ?Money + { + return $this->insurance; + } + + public function shippingDiscount(): ?Money + { + return $this->shippingDiscount; + } + + public function discount(): ?Money + { + return $this->discount; + } + + public function toArray(): array + { + $breakdown = []; + if ($this->itemTotal) { + $breakdown['item_total'] = $this->itemTotal->toArray(); + } + if ($this->shipping) { + $breakdown['shipping'] = $this->shipping->toArray(); + } + if ($this->taxTotal) { + $breakdown['tax_total'] = $this->taxTotal->toArray(); + } + if ($this->handling) { + $breakdown['handling'] = $this->handling->toArray(); + } + if ($this->insurance) { + $breakdown['insurance'] = $this->insurance->toArray(); + } + if ($this->shippingDiscount) { + $breakdown['shipping_discount'] = $this->shippingDiscount->toArray(); + } + if ($this->discount) { + $breakdown['discount'] = $this->discount->toArray(); + } + + return $breakdown; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/ApplicationContext.php b/modules.local/ppcp-api-client/src/Entity/ApplicationContext.php new file mode 100644 index 000000000..0de2a91c5 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/ApplicationContext.php @@ -0,0 +1,154 @@ +returnUrl = $returnUrl; + $this->cancelUrl = $cancelUrl; + $this->brandName = $brandName; + $this->locale = $locale; + $this->landingPage = $landingPage; + $this->shippingPreference = $shippingPreference; + $this->userAction = $userAction; + + //Currently we have not implemented the payment method. + $this->paymentMethod = null; + } + + public function brandName(): string + { + return $this->brandName; + } + + public function locale(): string + { + return $this->locale; + } + + public function landingPage(): string + { + return $this->landingPage; + } + + public function shippingPreference(): string + { + return $this->shippingPreference; + } + + public function userAction(): string + { + return $this->userAction; + } + + public function returnUrl(): string + { + return $this->returnUrl; + } + + public function cancelUrl(): string + { + return $this->cancelUrl; + } + + /** + * Currently, we have not implemented this. + * + * If we would follow our schema, we would create a paymentMethod entity which could + * get returned here. + */ + public function paymentMethod(): ?\stdClass + { + return $this->paymentMethod; + } + + public function toArray(): array + { + $data = []; + if ($this->userAction()) { + $data['user_action'] = $this->userAction(); + } + if ($this->paymentMethod()) { + $data['payment_method'] = $this->paymentMethod(); + } + if ($this->shippingPreference()) { + $data['shipping_preference'] = $this->shippingPreference(); + } + if ($this->landingPage()) { + $data['landing_page'] = $this->landingPage(); + } + if ($this->locale()) { + $data['locale'] = $this->locale(); + } + if ($this->brandName()) { + $data['brand_name'] = $this->brandName(); + } + if ($this->returnUrl()) { + $data['return_url'] = $this->returnUrl(); + } + if ($this->cancelUrl()) { + $data['cancel_url'] = $this->cancelUrl(); + } + if ($this->paymentMethod()) { + $data['payment_method'] = $this->paymentMethod(); + } + return $data; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Authorization.php b/modules.local/ppcp-api-client/src/Entity/Authorization.php new file mode 100644 index 000000000..7041ab815 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Authorization.php @@ -0,0 +1,38 @@ +id = $id; + $this->authorizationStatus = $authorizationStatus; + } + + public function id(): string + { + return $this->id; + } + + public function status(): AuthorizationStatus + { + return $this->authorizationStatus; + } + + public function toArray(): array + { + return [ + 'id' => $this->id, + 'status' => $this->authorizationStatus->name(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/AuthorizationStatus.php b/modules.local/ppcp-api-client/src/Entity/AuthorizationStatus.php new file mode 100644 index 000000000..5c3647f31 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/AuthorizationStatus.php @@ -0,0 +1,61 @@ +status = $status; + } + + public static function asInternal(): AuthorizationStatus + { + return new self(self::INTERNAL); + } + + public function is(string $status): bool + { + return $this->status === $status; + } + + public function name(): string + { + return $this->status; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/CardAuthenticationResult.php b/modules.local/ppcp-api-client/src/Entity/CardAuthenticationResult.php new file mode 100644 index 000000000..5eaf1f9b0 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/CardAuthenticationResult.php @@ -0,0 +1,71 @@ +liabilityShift = strtoupper($liabilityShift); + $this->enrollmentStatus = strtoupper($enrollmentStatus); + $this->authenticationResult = strtoupper($authenticationResult); + } + + public function liabilityShift(): string + { + + return $this->liabilityShift; + } + + public function enrollmentStatus(): string + { + + return $this->enrollmentStatus; + } + + public function authenticationResult(): string + { + + return $this->authenticationResult; + } + + public function toArray(): array + { + $data = []; + $data['liability_shift'] = $this->liabilityShift(); + $data['three_d_secure'] = [ + 'enrollment_status' => $this->enrollmentStatus(), + 'authentication_result' => $this->authenticationResult(), + ]; + return $data; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Item.php b/modules.local/ppcp-api-client/src/Entity/Item.php new file mode 100644 index 000000000..959c15e73 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Item.php @@ -0,0 +1,93 @@ +name = $name; + $this->unitAmount = $unitAmount; + $this->quantity = $quantity; + $this->description = $description; + $this->tax = $tax; + $this->sku = $sku; + $this->category = ($category === self::DIGITAL_GOODS) ? + self::DIGITAL_GOODS : self::PHYSICAL_GOODS; + } + + public function name(): string + { + return $this->name; + } + + public function unitAmount(): Money + { + return $this->unitAmount; + } + + public function quantity(): int + { + return $this->quantity; + } + + public function description(): string + { + return $this->description; + } + + public function tax(): ?Money + { + return $this->tax; + } + + public function sku(): string + { + return $this->sku; + } + + public function category(): string + { + return $this->category; + } + + public function toArray(): array + { + $item = [ + 'name' => $this->name(), + 'unit_amount' => $this->unitAmount()->toArray(), + 'quantity' => $this->quantity(), + 'description' => $this->description(), + 'sku' => $this->sku(), + 'category' => $this->category(), + ]; + + if ($this->tax()) { + $item['tax'] = $this->tax()->toArray(); + } + + return $item; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Money.php b/modules.local/ppcp-api-client/src/Entity/Money.php new file mode 100644 index 000000000..87f8b50ed --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Money.php @@ -0,0 +1,35 @@ +value = $value; + $this->currencyCode = $currencyCode; + } + + public function value(): float + { + return $this->value; + } + + public function currencyCode(): string + { + return $this->currencyCode; + } + + public function toArray(): array + { + return [ + 'currency_code' => $this->currencyCode(), + 'value' => number_format($this->value(), 2), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Order.php b/modules.local/ppcp-api-client/src/Entity/Order.php new file mode 100644 index 000000000..73326c7b0 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Order.php @@ -0,0 +1,137 @@ +id = $id; + $this->applicationContext = $applicationContext; + //phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration.NoArgumentType + $this->purchaseUnits = array_values(array_filter( + $purchaseUnits, + static function ($unit): bool { + return is_a($unit, PurchaseUnit::class); + } + )); + //phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration.NoArgumentType + $this->payer = $payer; + $this->orderStatus = $orderStatus; + $this->intent = ($intent === 'CAPTURE') ? 'CAPTURE' : 'AUTHORIZE'; + $this->purchaseUnits = $purchaseUnits; + $this->createTime = $createTime; + $this->updateTime = $updateTime; + $this->paymentSource = $paymentSource; + } + + public function id(): string + { + return $this->id; + } + + public function createTime(): ?\DateTime + { + return $this->createTime; + } + + public function updateTime(): ?\DateTime + { + return $this->updateTime; + } + + public function intent(): string + { + return $this->intent; + } + + public function payer(): ?Payer + { + return $this->payer; + } + + /** + * @return PurchaseUnit[] + */ + public function purchaseUnits(): array + { + return $this->purchaseUnits; + } + + public function status(): OrderStatus + { + return $this->orderStatus; + } + + public function applicationContext(): ?ApplicationContext + { + + return $this->applicationContext; + } + + public function paymentSource(): ?PaymentSource + { + + return $this->paymentSource; + } + + public function toArray(): array + { + $order = [ + 'id' => $this->id(), + 'intent' => $this->intent(), + 'status' => $this->status()->name(), + 'purchase_units' => array_map( + static function (PurchaseUnit $unit): array { + return $unit->toArray(); + }, + $this->purchaseUnits() + ), + ]; + if ($this->createTime()) { + $order['create_time'] = $this->createTime()->format(\DateTimeInterface::ISO8601); + } + if ($this->payer()) { + $order['payer'] = $this->payer()->toArray(); + } + if ($this->updateTime()) { + $order['update_time'] = $this->updateTime()->format(\DateTimeInterface::ISO8601); + } + if ($this->applicationContext()) { + $order['application_context'] = $this->applicationContext()->toArray(); + } + if ($this->paymentSource()) { + $order['payment_source'] = $this->paymentSource()->toArray(); + } + + return $order; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/OrderStatus.php b/modules.local/ppcp-api-client/src/Entity/OrderStatus.php new file mode 100644 index 000000000..4a014bec2 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/OrderStatus.php @@ -0,0 +1,54 @@ +status = $status; + } + + public static function asInternal(): OrderStatus + { + return new self(self::INTERNAL); + } + + public function is(string $status): bool + { + return $this->status === $status; + } + + public function name(): string + { + return $this->status; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Patch.php b/modules.local/ppcp-api-client/src/Entity/Patch.php new file mode 100644 index 000000000..22881a076 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Patch.php @@ -0,0 +1,57 @@ +op = $op; + $this->path = $path; + $this->value = $value; + } + + public function op(): string + { + return $this->op; + } + + public function path(): string + { + return $this->path; + } + + // phpcs:disable Inpsyde.CodeQuality.ReturnTypeDeclaration.NoReturnType + public function value() + { + return $this->value; + } + // phpcs:enable Inpsyde.CodeQuality.ReturnTypeDeclaration.NoReturnType + + public function toArray(): array + { + return [ + 'op' => $this->op(), + 'value' => $this->value(), + 'path' => $this->path(), + ]; + } + + /** + * Needed for the move operation. We currently do not + * support the move operation. + * + * @return string + */ + public function from(): string + { + return ''; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PatchCollection.php b/modules.local/ppcp-api-client/src/Entity/PatchCollection.php new file mode 100644 index 000000000..ced0eabec --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PatchCollection.php @@ -0,0 +1,33 @@ +patches = $patches; + } + + /** + * @return Patch[] + */ + public function patches(): array + { + return $this->patches; + } + + public function toArray(): array + { + return array_map( + static function (Patch $patch): array { + return $patch->toArray(); + }, + $this->patches() + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Payee.php b/modules.local/ppcp-api-client/src/Entity/Payee.php new file mode 100644 index 000000000..a4bb5e50c --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Payee.php @@ -0,0 +1,48 @@ +email = $email; + $this->merchantId = $merchantId; + } + + public function email(): string + { + return $this->email; + } + + public function merchantId(): string + { + return $this->merchantId; + } + + public function toArray(): array + { + $data = [ + 'email_address' => $this->email(), + ]; + if ($this->merchantId) { + $data['merchant_id'] = $this->merchantId(); + } + return $data; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Payer.php b/modules.local/ppcp-api-client/src/Entity/Payer.php new file mode 100644 index 000000000..ab0d6b8f3 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Payer.php @@ -0,0 +1,100 @@ +name = $name; + $this->emailAddress = $emailAddress; + $this->payerId = $payerId; + $this->birthDate = $birthDate; + $this->address = $address; + $this->phone = $phone; + $this->taxInfo = $taxInfo; + } + + public function name(): PayerName + { + return $this->name; + } + + public function emailAddress(): string + { + return $this->emailAddress; + } + + public function payerId(): string + { + return $this->payerId; + } + + public function birthDate(): ?\DateTime + { + return $this->birthDate; + } + + public function address(): Address + { + return $this->address; + } + + public function phone(): ?PhoneWithType + { + return $this->phone; + } + + public function taxInfo(): ?PayerTaxInfo + { + return $this->taxInfo; + } + + public function toArray(): array + { + $payer = [ + 'name' => $this->name()->toArray(), + 'email_address' => $this->emailAddress(), + 'address' => $this->address()->toArray(), + ]; + if ($this->payerId()) { + $payer['payer_id'] = $this->payerId(); + } + + if ($this->phone()) { + $payer['phone'] = $this->phone()->toArray(); + } + if ($this->taxInfo()) { + $payer['tax_info'] = $this->taxInfo()->toArray(); + } + if ($this->birthDate()) { + $payer['birth_date'] = $this->birthDate()->format('Y-m-d'); + } + return $payer; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PayerName.php b/modules.local/ppcp-api-client/src/Entity/PayerName.php new file mode 100644 index 000000000..aae545f26 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PayerName.php @@ -0,0 +1,39 @@ +givenName = $givenName; + $this->surname = $surname; + } + + public function givenName(): string + { + return $this->givenName; + } + + public function surname(): string + { + return $this->surname; + } + + public function toArray(): array + { + return [ + 'given_name' => $this->givenName(), + 'surname' => $this->surname(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PayerTaxInfo.php b/modules.local/ppcp-api-client/src/Entity/PayerTaxInfo.php new file mode 100644 index 000000000..348df8f98 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PayerTaxInfo.php @@ -0,0 +1,51 @@ +taxId = $taxId; + $this->type = $type; + } + + public function type(): string + { + return $this->type; + } + + public function taxId(): string + { + return $this->taxId; + } + + public function toArray(): array + { + return [ + 'tax_id' => $this->taxId(), + 'tax_id_type' => $this->type(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PaymentMethod.php b/modules.local/ppcp-api-client/src/Entity/PaymentMethod.php new file mode 100644 index 000000000..8f2df0961 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PaymentMethod.php @@ -0,0 +1,43 @@ +preferred = $preferred; + $this->selected = $selected; + } + + public function payeePreferred(): string + { + return $this->preferred; + } + + public function payerSelected(): string + { + return $this->selected; + } + + public function toArray(): array + { + return [ + 'payee_preferred' => $this->payeePreferred(), + 'payer_selected' => $this->payerSelected(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PaymentSource.php b/modules.local/ppcp-api-client/src/Entity/PaymentSource.php new file mode 100644 index 000000000..7df30c99b --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PaymentSource.php @@ -0,0 +1,45 @@ +card = $card; + $this->wallet = $wallet; + } + + public function card(): ?PaymentSourceCard + { + + return $this->card; + } + + public function wallet(): ?PaymentSourceWallet + { + + return $this->wallet; + } + + public function toArray(): array + { + + $data = []; + if ($this->card()) { + $data['card'] = $this->card()->toArray(); + } + if ($this->wallet()) { + $data['wallet'] = $this->wallet()->toArray(); + } + return $data; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PaymentSourceCard.php b/modules.local/ppcp-api-client/src/Entity/PaymentSourceCard.php new file mode 100644 index 000000000..820e929f1 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PaymentSourceCard.php @@ -0,0 +1,64 @@ +lastDigits = $lastDigits; + $this->brand = $brand; + $this->type = $type; + $this->authenticationResult = $authenticationResult; + } + + public function lastDigits(): string + { + + return $this->lastDigits; + } + + public function brand(): string + { + + return $this->brand; + } + + public function type(): string + { + + return $this->type; + } + + public function authenticationResult(): ?CardAuthenticationResult + { + + return $this->authenticationResult; + } + + public function toArray(): array + { + + $data = [ + 'last_digits' => $this->lastDigits(), + 'brand' => $this->brand(), + 'type' => $this->type(), + ]; + if ($this->authenticationResult()) { + $data['authentication_result'] = $this->authenticationResult()->toArray(); + } + return $data; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PaymentSourceWallet.php b/modules.local/ppcp-api-client/src/Entity/PaymentSourceWallet.php new file mode 100644 index 000000000..1cf012e6d --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PaymentSourceWallet.php @@ -0,0 +1,14 @@ +id = $id; + $this->type = $type; + } + + public function id(): string + { + return $this->id; + } + + public function type(): string + { + return $this->type; + } + + public function toArray(): array + { + return [ + 'id' => $this->id(), + 'type' => $this->type(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Payments.php b/modules.local/ppcp-api-client/src/Entity/Payments.php new file mode 100644 index 000000000..890e3f61f --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Payments.php @@ -0,0 +1,35 @@ +authorizations = $authorizations; + } + + public function toArray(): array + { + return [ + 'authorizations' => array_map( + static function (Authorization $authorization): array { + return $authorization->toArray(); + }, + $this->authorizations() + ), + ]; + } + + /** + * @return Authorization[] + **/ + public function authorizations(): array + { + return $this->authorizations; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Phone.php b/modules.local/ppcp-api-client/src/Entity/Phone.php new file mode 100644 index 000000000..193eab138 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Phone.php @@ -0,0 +1,27 @@ +nationalNumber = $nationalNumber; + } + + public function nationalNumber(): string + { + return $this->nationalNumber; + } + + public function toArray(): array + { + return [ + 'national_number' => $this->nationalNumber(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PhoneWithType.php b/modules.local/ppcp-api-client/src/Entity/PhoneWithType.php new file mode 100644 index 000000000..59dee823e --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PhoneWithType.php @@ -0,0 +1,42 @@ +type = in_array($type, self::VALLID_TYPES, true) ? $type : 'OTHER'; + $this->phone = $phone; + } + + public function type(): string + { + return $this->type; + } + + public function phone(): Phone + { + return $this->phone; + } + + public function toArray(): array + { + return [ + 'phone_type' => $this->type(), + 'phone_number' => $this->phone()->toArray(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules.local/ppcp-api-client/src/Entity/PurchaseUnit.php new file mode 100644 index 000000000..62e9db5d8 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -0,0 +1,231 @@ +amount = $amount; + $this->shipping = $shipping; + $this->referenceId = $referenceId; + $this->description = $description; + //phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration.NoArgumentType + $this->items = array_values(array_filter( + $items, + function ($item): bool { + $isItem = is_a($item, Item::class); + /** + * @var Item $item + */ + if ($isItem && Item::PHYSICAL_GOODS === $item->category()) { + $this->containsPhysicalGoodsItems = true; + } + + return $isItem; + } + )); + //phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration.NoArgumentType + $this->payee = $payee; + $this->customId = $customId; + $this->invoiceId = $invoiceId; + $this->softDescriptor = $softDescriptor; + $this->payments = $payments; + } + + public function amount(): Amount + { + return $this->amount; + } + + public function shipping(): ?Shipping + { + return $this->shipping; + } + + public function referenceId(): string + { + return $this->referenceId; + } + + public function description(): string + { + return $this->description; + } + + public function customId(): string + { + return $this->customId; + } + + public function invoiceId(): string + { + return $this->invoiceId; + } + + public function softDescriptor(): string + { + return $this->softDescriptor; + } + + public function payee(): ?Payee + { + return $this->payee; + } + + public function payments(): ?Payments + { + return $this->payments; + } + + /** + * @return Item[] + */ + public function items(): array + { + return $this->items; + } + + public function containsPhysicalGoodsItems(): bool + { + return $this->containsPhysicalGoodsItems; + } + + public function toArray(): array + { + $purchaseUnit = [ + 'reference_id' => $this->referenceId(), + 'amount' => $this->amount()->toArray(), + 'description' => $this->description(), + 'items' => array_map( + static function (Item $item): array { + return $item->toArray(); + }, + $this->items() + ), + ]; + if ($this->ditchItemsWhenMismatch($this->amount(), ...$this->items())) { + unset($purchaseUnit['items']); + unset($purchaseUnit['amount']['breakdown']); + } + + if ($this->payee()) { + $purchaseUnit['payee'] = $this->payee()->toArray(); + } + + if ($this->payments()) { + $purchaseUnit['payments'] = $this->payments()->toArray(); + } + + if ($this->shipping()) { + $purchaseUnit['shipping'] = $this->shipping()->toArray(); + } + if ($this->customId()) { + $purchaseUnit['custom_id'] = $this->customId(); + } + if ($this->invoiceId()) { + $purchaseUnit['invoice_id'] = $this->invoiceId(); + } + if ($this->softDescriptor()) { + $purchaseUnit['soft_descriptor'] = $this->softDescriptor(); + } + return $purchaseUnit; + } + + /** + * All money values send to PayPal can only have 2 decimal points. Woocommerce internally does + * not have this restriction. Therefore the totals of the cart in Woocommerce and the totals + * of the rounded money values of the items, we send to PayPal, can differ. In those cases, + * we can not send the line items. + * + * @param Amount $amount + * @param Item ...$items + * @return bool + */ + //phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh + private function ditchItemsWhenMismatch(Amount $amount, Item ...$items): bool + { + $feeItemsTotal = ($amount->breakdown() && $amount->breakdown()->itemTotal()) ? + $amount->breakdown()->itemTotal()->value() : null; + $feeTaxTotal = ($amount->breakdown() && $amount->breakdown()->taxTotal()) ? + $amount->breakdown()->taxTotal()->value() : null; + + foreach ($items as $item) { + if (null !== $feeItemsTotal) { + $feeItemsTotal -= $item->unitAmount()->value() * $item->quantity(); + } + if (null !== $feeTaxTotal) { + $feeTaxTotal -= $item->tax()->value() * $item->quantity(); + } + } + + $feeItemsTotal = round($feeItemsTotal, 2); + $feeTaxTotal = round($feeTaxTotal, 2); + + if ($feeItemsTotal !== 0.0 || $feeTaxTotal !== 0.0) { + return true; + } + + $breakdown = $this->amount()->breakdown(); + if (! $breakdown) { + return false; + } + $amountTotal = 0; + if ($breakdown->shipping()) { + $amountTotal += $breakdown->shipping()->value(); + } + if ($breakdown->itemTotal()) { + $amountTotal += $breakdown->itemTotal()->value(); + } + if ($breakdown->discount()) { + $amountTotal -= $breakdown->discount()->value(); + } + if ($breakdown->taxTotal()) { + $amountTotal += $breakdown->taxTotal()->value(); + } + if ($breakdown->shippingDiscount()) { + $amountTotal -= $breakdown->shippingDiscount()->value(); + } + if ($breakdown->handling()) { + $amountTotal += $breakdown->handling()->value(); + } + if ($breakdown->insurance()) { + $amountTotal += $breakdown->insurance()->value(); + } + + $amountValue = $this->amount()->value(); + $needsToDitch = (string) $amountTotal !== (string) $amountValue; + return $needsToDitch; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Shipping.php b/modules.local/ppcp-api-client/src/Entity/Shipping.php new file mode 100644 index 000000000..c989da3ca --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Shipping.php @@ -0,0 +1,37 @@ +name = $name; + $this->address = $address; + } + + public function name(): string + { + return $this->name; + } + + public function address(): Address + { + return $this->address; + } + + public function toArray(): array + { + return [ + 'name' => [ + 'full_name' => $this->name(), + ], + 'address' => $this->address()->toArray(), + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Token.php b/modules.local/ppcp-api-client/src/Entity/Token.php new file mode 100644 index 000000000..6c4f86885 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Token.php @@ -0,0 +1,72 @@ +created)) { + $json->created = time(); + } + if (! $this->validate($json)) { + throw new RuntimeException("Token not valid"); + } + $this->json = $json; + } + + public function expirationTimestamp(): int + { + + return $this->json->created + $this->json->expires_in; + } + + public function token(): string + { + return (string) $this->json->token; + } + + public function isValid(): bool + { + return time() < $this->json->created + $this->json->expires_in; + } + + public function asJson(): string + { + return json_encode($this->json); + } + + public static function fromJson(string $json): self + { + $json = (object) json_decode($json); + if (isset($json->access_token) || isset($json->client_token)) { + $json->token = isset($json->access_token) ? $json->access_token : $json->client_token; + } + + return new Token($json); + } + + private function validate(\stdClass $json): bool + { + $propertyMap = [ + 'created' => 'is_int', + 'expires_in' => 'is_int', + 'token' => 'is_string', + ]; + + foreach ($propertyMap as $property => $validator) { + if (! isset($json->{$property}) || ! $validator($json->{$property})) { + return false; + } + } + return true; + } +} diff --git a/modules.local/ppcp-api-client/src/Entity/Webhook.php b/modules.local/ppcp-api-client/src/Entity/Webhook.php new file mode 100644 index 000000000..fdabe4690 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Entity/Webhook.php @@ -0,0 +1,50 @@ +url = $url; + $this->eventTypes = $eventTypes; + $this->id = $id; + } + + public function id(): string + { + + return $this->id; + } + + public function url(): string + { + + return $this->url; + } + + public function eventTypes(): array + { + + return $this->eventTypes; + } + + public function toArray(): array + { + + $data = [ + 'url' => $this->url(), + 'event_types' => $this->eventTypes(), + ]; + if ($this->id()) { + $data['id'] = $this->id(); + } + return $data; + } +} diff --git a/modules.local/ppcp-api-client/src/Exception/NotFoundException.php b/modules.local/ppcp-api-client/src/Exception/NotFoundException.php new file mode 100644 index 000000000..468270369 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Exception/NotFoundException.php @@ -0,0 +1,13 @@ +message)) { + $response->message = __( + 'Unknown error while connecting to PayPal.', + 'woocommerce-paypal-commerce-gateway' + ); + } + if (! isset($response->name)) { + $response->name = __('Error', 'woocommerce-paypal-commerce-gateway'); + } + if (! isset($response->details)) { + $response->details = []; + } + if (! isset($response->links) || ! is_array($response->links)) { + $response->links = []; + } + + $this->response = $response; + $this->statusCode = $statusCode; + $message = $response->message; + if ($response->name) { + $message = '[' . $response->name . '] ' . $message; + } + foreach ($response->links as $link) { + if (isset($link->rel) && $link->rel === 'information_link') { + $message .= ' ' . $link->href; + } + } + parent::__construct($message, $statusCode); + } + + public function name(): string + { + return $this->response->name; + } + + public function details(): array + { + return $this->response->details; + } + + public function hasDetail(string $issue): bool + { + foreach ($this->details() as $detail) { + if (isset($detail->issue) && $detail->issue === $issue) { + return true; + } + } + return false; + } +} diff --git a/modules.local/ppcp-api-client/src/Exception/RuntimeException.php b/modules.local/ppcp-api-client/src/Exception/RuntimeException.php new file mode 100644 index 000000000..c122fe07d --- /dev/null +++ b/modules.local/ppcp-api-client/src/Exception/RuntimeException.php @@ -0,0 +1,10 @@ +get_shipping_country() : $customer->get_billing_country(), + ($type === 'shipping') ? + $customer->get_shipping_address_1() : $customer->get_billing_address_1(), + ($type === 'shipping') ? + $customer->get_shipping_address_2() : $customer->get_billing_address_2(), + ($type === 'shipping') ? + $customer->get_shipping_state() : $customer->get_billing_state(), + ($type === 'shipping') ? + $customer->get_shipping_city() : $customer->get_billing_city(), + ($type === 'shipping') ? + $customer->get_shipping_postcode() : $customer->get_billing_postcode(), + ); + } + + public function fromWcOrder(\WC_Order $order): Address + { + return new Address( + $order->get_shipping_country(), + $order->get_shipping_address_1(), + $order->get_shipping_address_2(), + $order->get_shipping_state(), + $order->get_shipping_city(), + $order->get_shipping_postcode() + ); + } + + public function fromPayPalRequest(\stdClass $data): Address + { + if (! isset($data->country_code)) { + throw new RuntimeException( + __('No country given for address.', 'woocommerce-paypal-commerce-gateway') + ); + } + return new Address( + $data->country_code, + (isset($data->address_line_1)) ? $data->address_line_1 : '', + (isset($data->address_line_2)) ? $data->address_line_2 : '', + (isset($data->admin_area_1)) ? $data->admin_area_1 : '', + (isset($data->admin_area_2)) ? $data->admin_area_2 : '', + (isset($data->postal_code)) ? $data->postal_code : '' + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/AmountFactory.php b/modules.local/ppcp-api-client/src/Factory/AmountFactory.php new file mode 100644 index 000000000..5092328e1 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/AmountFactory.php @@ -0,0 +1,169 @@ +itemFactory = $itemFactory; + } + + public function fromWcCart(\WC_Cart $cart): Amount + { + $currency = get_woocommerce_currency(); + $total = new Money((float) $cart->get_total('numeric'), $currency); + $itemsTotal = $cart->get_cart_contents_total() + $cart->get_discount_total(); + $itemsTotal = new Money((float) $itemsTotal, $currency); + $shipping = new Money( + (float) $cart->get_shipping_total() + $cart->get_shipping_tax(), + $currency + ); + + $taxes = new Money( + (float) $cart->get_cart_contents_tax() + (float) $cart->get_discount_tax(), + $currency + ); + + $discount = null; + if ($cart->get_discount_total()) { + $discount = new Money( + (float) $cart->get_discount_total() + $cart->get_discount_tax(), + $currency + ); + } + //ToDo: Evaluate if more is needed? Fees? + $breakdown = new AmountBreakdown( + $itemsTotal, + $shipping, + $taxes, + null, // insurance? + null, // handling? + null, //shipping discounts? + $discount + ); + $amount = new Amount( + $total, + $breakdown + ); + return $amount; + } + + public function fromWcOrder(\WC_Order $order): Amount + { + $currency = $order->get_currency(); + $items = $this->itemFactory->fromWcOrder($order); + $total = new Money((float) $order->get_total(), $currency); + $itemsTotal = new Money((float)array_reduce( + $items, + static function (float $total, Item $item): float { + return $total + $item->quantity() * $item->unitAmount()->value(); + }, + 0 + ), $currency); + $shipping = new Money( + (float) $order->get_shipping_total() + (float) $order->get_shipping_tax(), + $currency + ); + $taxes = new Money((float)array_reduce( + $items, + static function (float $total, Item $item): float { + return $total + $item->quantity() * $item->tax()->value(); + }, + 0 + ), $currency); + + $discount = null; + if ((float) $order->get_total_discount(false)) { + $discount = new Money( + (float) $order->get_total_discount(false), + $currency + ); + } + + //ToDo: Evaluate if more is needed? Fees? + $breakdown = new AmountBreakdown( + $itemsTotal, + $shipping, + $taxes, + null, // insurance? + null, // handling? + null, //shipping discounts? + $discount + ); + $amount = new Amount( + $total, + $breakdown + ); + return $amount; + } + + public function fromPayPalResponse(\stdClass $data): Amount + { + if (! isset($data->value) || ! is_numeric($data->value)) { + throw new RuntimeException(__("No value given", "woocommerce-paypal-commerce-gateway")); + } + if (! isset($data->currency_code)) { + throw new RuntimeException( + __("No currency given", "woocommerce-paypal-commerce-gateway") + ); + } + + $money = new Money((float) $data->value, $data->currency_code); + $breakdown = (isset($data->breakdown)) ? $this->breakdown($data->breakdown) : null; + return new Amount($money, $breakdown); + } + + private function breakDown(\stdClass $data): AmountBreakdown + { + /** + * The order of the keys equals the necessary order of the constructor arguments. + */ + $orderedConstructorKeys = [ + 'item_total', + 'shipping', + 'tax_total', + 'handling', + 'insurance', + 'shipping_discount', + 'discount', + ]; + + $money = []; + foreach ($orderedConstructorKeys as $key) { + if (! isset($data->{$key})) { + $money[] = null; + continue; + } + $item = $data->{$key}; + + if (! isset($item->value) || ! is_numeric($item->value)) { + throw new RuntimeException(sprintf( + // translators: %s is the current breakdown key. + __("No value given for breakdown %s", "woocommerce-paypal-commerce-gateway"), + $key + )); + } + if (! isset($item->currency_code)) { + throw new RuntimeException(sprintf( + // translators: %s is the current breakdown key. + __("No currency given for breakdown %s", "woocommerce-paypal-commerce-gateway"), + $key + )); + } + $money[] = new Money((float) $item->value, $item->currency_code); + } + + return new AmountBreakdown(...$money); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/ApplicationContextFactory.php b/modules.local/ppcp-api-client/src/Factory/ApplicationContextFactory.php new file mode 100644 index 000000000..00bb733d4 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/ApplicationContextFactory.php @@ -0,0 +1,31 @@ +return_url) ? + $data->return_url : '', + isset($data->cancel_url) ? + $data->cancel_url : '', + isset($data->brand_name) ? + $data->brand_name : '', + isset($data->locale) ? + $data->locale : '', + isset($data->landing_page) ? + $data->landing_page : ApplicationContext::LANDING_PAGE_NO_PREFERENCE, + isset($data->shipping_preference) ? + $data->shipping_preference : ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE, + isset($data->user_action) ? + $data->user_action : ApplicationContext::USER_ACTION_CONTINUE, + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/AuthorizationFactory.php b/modules.local/ppcp-api-client/src/Factory/AuthorizationFactory.php new file mode 100644 index 000000000..be004f406 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/AuthorizationFactory.php @@ -0,0 +1,32 @@ +id)) { + throw new RuntimeException( + __('Does not contain an id.', 'woocommerce-paypal-commerce-gateway') + ); + } + + if (!isset($data->status)) { + throw new RuntimeException( + __('Does not contain status.', 'woocommerce-paypal-commerce-gateway') + ); + } + + return new Authorization( + $data->id, + new AuthorizationStatus($data->status) + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/ItemFactory.php b/modules.local/ppcp-api-client/src/Factory/ItemFactory.php new file mode 100644 index 000000000..7413b85dc --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/ItemFactory.php @@ -0,0 +1,124 @@ +get_name(), 0, 127), + new Money($priceWithoutTaxRounded, $currency), + $quantity, + mb_substr($product->get_description(), 0, 127), + $tax, + $product->get_sku(), + ($product->is_virtual()) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS + ); + }, + $cart->get_cart_contents() + ); + return $items; + } + + /** + * @param \WC_Order $order + * @return Item[] + */ + public function fromWcOrder(\WC_Order $order): array + { + return array_map( + function (\WC_Order_Item_Product $item) use ($order): Item { + return $this->fromWcOrderLineItem($item, $order); + }, + $order->get_items('line_item') + ); + } + + private function fromWcOrderLineItem(\WC_Order_Item_Product $item, \WC_Order $order): Item + { + $currency = $order->get_currency(); + $product = $item->get_product(); + /** + * @var \WC_Product $product + */ + $quantity = $item->get_quantity(); + + $price = (float) $order->get_item_subtotal($item, true); + $priceWithoutTax = (float) $order->get_item_subtotal($item, false); + $priceWithoutTaxRounded = round($priceWithoutTax, 2); + $tax = round($price - $priceWithoutTaxRounded, 2); + $tax = new Money($tax, $currency); + return new Item( + mb_substr($product->get_name(), 0, 127), + new Money($priceWithoutTaxRounded, $currency), + $quantity, + mb_substr($product->get_description(), 0, 127), + $tax, + $product->get_sku(), + ($product->is_virtual()) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS + ); + } + + public function fromPayPalResponse(\stdClass $data): Item + { + if (! isset($data->name)) { + throw new RuntimeException( + __("No name for item given", "woocommerce-paypal-commerce-gateway") + ); + } + if (! isset($data->quantity) || ! is_numeric($data->quantity)) { + throw new RuntimeException( + __("No quantity for item given", "woocommerce-paypal-commerce-gateway") + ); + } + if (! isset($data->unit_amount->value) || ! isset($data->unit_amount->currency_code)) { + throw new RuntimeException( + __("No money values for item given", "woocommerce-paypal-commerce-gateway") + ); + } + + $unitAmount = new Money((float) $data->unit_amount->value, $data->unit_amount->currency_code); + $description = (isset($data->description)) ? $data->description : ''; + $tax = (isset($data->tax)) ? + new Money((float) $data->tax->value, $data->tax->currency_code) + : null; + $sku = (isset($data->sku)) ? $data->sku : ''; + $category = (isset($data->category)) ? $data->category : 'PHYSICAL_GOODS'; + + return new Item( + $data->name, + $unitAmount, + (int) $data->quantity, + $description, + $tax, + $sku, + $category + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/OrderFactory.php b/modules.local/ppcp-api-client/src/Factory/OrderFactory.php new file mode 100644 index 000000000..119e667f9 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/OrderFactory.php @@ -0,0 +1,113 @@ +purchaseUnitFactory = $purchaseUnitFactory; + $this->payerFactory = $payerFactory; + $this->applicationContextRepository = $applicationContextRepository; + $this->applicationContextFactory = $applicationContextFactory; + $this->paymentSourceFactory = $paymentSourceFactory; + } + + public function fromWcOrder(\WC_Order $wcOrder, Order $order): Order + { + $purchaseUnits = [$this->purchaseUnitFactory->fromWcOrder($wcOrder)]; + + return new Order( + $order->id(), + $purchaseUnits, + $order->status(), + $order->applicationContext(), + $order->paymentSource(), + $order->payer(), + $order->intent(), + $order->createTime(), + $order->updateTime() + ); + } + + // phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong + public function fromPayPalResponse(\stdClass $orderData): Order + { + if (! isset($orderData->id)) { + throw new RuntimeException( + __('Order does not contain an id.', 'woocommerce-paypal-commerce-gateway') + ); + } + if (! isset($orderData->purchase_units) || !is_array($orderData->purchase_units)) { + throw new RuntimeException( + __('Order does not contain items.', 'woocommerce-paypal-commerce-gateway') + ); + } + if (! isset($orderData->status)) { + throw new RuntimeException( + __('Order does not contain status.', 'woocommerce-paypal-commerce-gateway') + ); + } + if (! isset($orderData->intent)) { + throw new RuntimeException( + __('Order does not contain intent.', 'woocommerce-paypal-commerce-gateway') + ); + } + + $purchaseUnits = array_map( + function (\stdClass $data): PurchaseUnit { + return $this->purchaseUnitFactory->fromPayPalResponse($data); + }, + $orderData->purchase_units + ); + + $createTime = (isset($orderData->create_time)) ? + \DateTime::createFromFormat(\DateTime::ISO8601, $orderData->create_time) + : null; + $updateTime = (isset($orderData->update_time)) ? + \DateTime::createFromFormat(\DateTime::ISO8601, $orderData->update_time) + : null; + $payer = (isset($orderData->payer)) ? + $this->payerFactory->fromPayPalResponse($orderData->payer) + : null; + $applicationContext = (isset($orderData->application_context)) ? + $this->applicationContextFactory->fromPayPalResponse($orderData->application_context) + : null; + $paymentSource = (isset($orderData->payment_source)) ? + $this->paymentSourceFactory->fromPayPalResponse($orderData->payment_source) : + null; + + return new Order( + $orderData->id, + $purchaseUnits, + new OrderStatus($orderData->status), + $applicationContext, + $paymentSource, + $payer, + $orderData->intent, + $createTime, + $updateTime + ); + } + // phpcs:enable Inpsyde.CodeQuality.FunctionLength.TooLong +} diff --git a/modules.local/ppcp-api-client/src/Factory/PatchCollectionFactory.php b/modules.local/ppcp-api-client/src/Factory/PatchCollectionFactory.php new file mode 100644 index 000000000..37f073f54 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/PatchCollectionFactory.php @@ -0,0 +1,66 @@ +purchaseUnits($from->purchaseUnits(), $to->purchaseUnits()); + + return new PatchCollection(...$allPatches); + } + + /** + * @param PurchaseUnit[] $from + * @param PurchaseUnit[] $to + * @return Patch[] + */ + private function purchaseUnits(array $from, array $to): array + { + $patches = []; + + $path = '/purchase_units'; + foreach ($to as $purchaseUnitTo) { + $needsUpdate = ! count( + array_filter( + $from, + static function (PurchaseUnit $unit) use ($purchaseUnitTo): bool { + //phpcs:disable WordPress.PHP.StrictComparisons.LooseComparison + // Loose comparison needed to compare two objects. + return $unit == $purchaseUnitTo; + //phpcs:enable WordPress.PHP.StrictComparisons.LooseComparison + } + ) + ); + $needsUpdate = true; + if (!$needsUpdate) { + continue; + } + $purchaseUnitFrom = current(array_filter( + $from, + static function (PurchaseUnit $unit) use ($purchaseUnitTo): bool { + return $purchaseUnitTo->referenceId() === $unit->referenceId(); + } + )); + $operation = $purchaseUnitFrom ? 'replace' : 'add'; + $value = $purchaseUnitTo->toArray(); + $patches[] = new Patch( + $operation, + $path . "/@reference_id=='" . $purchaseUnitTo->referenceId() . "'", + $value + ); + } + + return $patches; + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/PayeeFactory.php b/modules.local/ppcp-api-client/src/Factory/PayeeFactory.php new file mode 100644 index 000000000..82deece62 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/PayeeFactory.php @@ -0,0 +1,24 @@ +email_address)) { + throw new RuntimeException( + __("No email for payee given.", "woocommerce-paypal-commerce-gateway") + ); + } + + $merchantId = (isset($data->merchant_id)) ? $data->merchant_id : ''; + return new Payee($data->email_address, $merchantId); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/PayerFactory.php b/modules.local/ppcp-api-client/src/Factory/PayerFactory.php new file mode 100644 index 000000000..b6c4bd4e1 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/PayerFactory.php @@ -0,0 +1,82 @@ +addressFactory = $addressFactory; + } + + public function fromCustomer(\WC_Customer $customer): Payer + { + $payerId = ''; + $birthdate = null; + + $phone = null; + if ($customer->get_billing_phone()) { + // make sure the phone number contains only numbers and is max 14. chars long. + $nationalNumber = $customer->get_billing_phone(); + $nationalNumber = preg_replace("/[^0-9]/", "", $nationalNumber); + $nationalNumber = substr($nationalNumber, 0, 14); + + $phone = new PhoneWithType( + 'HOME', + new Phone($nationalNumber) + ); + } + return new Payer( + new PayerName( + $customer->get_billing_first_name(), + $customer->get_billing_last_name() + ), + $customer->get_billing_email(), + $payerId, + $this->addressFactory->fromWcCustomer($customer, 'billing'), + $birthdate, + $phone + ); + } + + public function fromPayPalResponse(\stdClass $data): Payer + { + $address = $this->addressFactory->fromPayPalRequest($data->address); + $payerName = new PayerName( + isset($data->name->given_name) ? (string) $data->name->given_name : '', + isset($data->name->surname) ? (string) $data->name->surname : '' + ); + // TODO deal with phones without type instead of passing a invalid type + $phone = (isset($data->phone)) ? new PhoneWithType( + (isset($data->phone->phone_type)) ? $data->phone->phone_type : 'undefined', + new Phone( + $data->phone->phone_number->national_number + ) + ) : null; + $taxInfo = (isset($data->tax_info)) ? + new PayerTaxInfo($data->tax_info->tax_id, $data->tax_info->tax_id_type) + : null; + $birthDate = (isset($data->birth_date)) ? + \DateTime::createFromFormat('Y-m-d', $data->birth_date) + : null; + return new Payer( + $payerName, + isset($data->email_address) ? $data->email_address : '', + (isset($data->payer_id)) ? $data->payer_id : '', + $address, + $birthDate, + $phone, + $taxInfo + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/PaymentSourceFactory.php b/modules.local/ppcp-api-client/src/Factory/PaymentSourceFactory.php new file mode 100644 index 000000000..e63645dbc --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/PaymentSourceFactory.php @@ -0,0 +1,40 @@ +card)) { + $authenticationResult = null; + if (isset($data->card->authentication_result)) { + $authenticationResult = new CardAuthenticationResult( + isset($data->card->authentication_result->liability_shift) ? + (string) $data->card->authentication_result->liability_shift : '', + isset($data->card->authentication_result->three_d_secure->enrollment_status) ? + (string) $data->card->authentication_result->three_d_secure->enrollment_status : '', + isset($data->card->authentication_result->three_d_secure->authentication_result) ? + (string) $data->card->authentication_result->three_d_secure->authentication_result : '' + ); + } + $card = new PaymentSourceCard( + isset($data->card->last_digits) ? (string) $data->card->last_digits : '', + isset($data->card->brand) ? (string) $data->card->brand : '', + isset($data->card->type) ? (string) $data->card->type : '', + $authenticationResult + ); + } + return new PaymentSource($card, $wallet); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/PaymentTokenFactory.php b/modules.local/ppcp-api-client/src/Factory/PaymentTokenFactory.php new file mode 100644 index 000000000..50bd070f2 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/PaymentTokenFactory.php @@ -0,0 +1,30 @@ +id)) { + throw new RuntimeException( + __("No id for payment token given", "woocommerce-paypal-commerce-gateway") + ); + } + return new PaymentToken( + $data->id, + (isset($data->type)) ? $data->type : PaymentToken::TYPE_PAYMENT_METHOD_TOKEN + ); + } + + public function fromArray(array $data): PaymentToken + { + return $this->fromPayPalResponse((object) $data); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/PaymentsFactory.php b/modules.local/ppcp-api-client/src/Factory/PaymentsFactory.php new file mode 100644 index 000000000..ddb17322e --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/PaymentsFactory.php @@ -0,0 +1,32 @@ +authorizationsFactory = $authorizationsFactory; + } + + public function fromPayPalResponse(\stdClass $data): Payments + { + $authorizations = array_map( + function (\stdClass $authorization): Authorization { + return $this->authorizationsFactory->fromPayPalRequest($authorization); + }, + isset($data->authorizations) ? $data->authorizations : [] + ); + $payments = new Payments(...$authorizations); + return $payments; + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules.local/ppcp-api-client/src/Factory/PurchaseUnitFactory.php new file mode 100644 index 000000000..7dbfe5794 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -0,0 +1,166 @@ +amountFactory = $amountFactory; + $this->payeeRepository = $payeeRepository; + $this->payeeFactory = $payeeFactory; + $this->itemFactory = $itemFactory; + $this->shippingFactory = $shippingFactory; + $this->paymentsFactory = $paymentsFactory; + $this->prefix = $prefix; + } + + public function fromWcOrder(\WC_Order $order): PurchaseUnit + { + $amount = $this->amountFactory->fromWcOrder($order); + $items = $this->itemFactory->fromWcOrder($order); + $shipping = $this->shippingFactory->fromWcOrder($order); + if ( + empty($shipping->address()->countryCode()) || + ($shipping->address()->countryCode() && !$shipping->address()->postalCode()) + ) { + $shipping = null; + } + $referenceId = 'default'; + $description = ''; + $payee = $this->payeeRepository->payee(); + $wcOrderId = $order->get_id(); + $customId = $this->prefix . $wcOrderId; + $invoiceId = $this->prefix . $wcOrderId; + $softDescriptor = ''; + $purchaseUnit = new PurchaseUnit( + $amount, + $items, + $shipping, + $referenceId, + $description, + $payee, + $customId, + $invoiceId, + $softDescriptor + ); + return $purchaseUnit; + } + + public function fromWcCart(\WC_Cart $cart): PurchaseUnit + { + $amount = $this->amountFactory->fromWcCart($cart); + $items = $this->itemFactory->fromWcCart($cart); + + $shipping = null; + $customer = \WC()->customer; + if (is_a($customer, \WC_Customer::class)) { + $shipping = $this->shippingFactory->fromWcCustomer(\WC()->customer); + if ( + ! $shipping->address()->countryCode() + || ($shipping->address()->countryCode() && !$shipping->address()->postalCode()) + ) { + $shipping = null; + } + } + + $referenceId = 'default'; + $description = ''; + + $payee = $this->payeeRepository->payee(); + + $customId = ''; + $invoiceId = ''; + $softDescriptor = ''; + $purchaseUnit = new PurchaseUnit( + $amount, + $items, + $shipping, + $referenceId, + $description, + $payee, + $customId, + $invoiceId, + $softDescriptor + ); + + return $purchaseUnit; + } + + public function fromPayPalResponse(\stdClass $data): PurchaseUnit + { + if (! isset($data->reference_id) || ! is_string($data->reference_id)) { + throw new RuntimeException( + __("No reference ID given.", "woocommercepaypal-commerce-gateway") + ); + } + + $amount = $this->amountFactory->fromPayPalResponse($data->amount); + $description = (isset($data->description)) ? $data->description : ''; + $customId = (isset($data->custom_id)) ? $data->custom_id : ''; + $invoiceId = (isset($data->invoice_id)) ? $data->invoice_id : ''; + $softDescriptor = (isset($data->soft_descriptor)) ? $data->soft_descriptor : ''; + $items = []; + if (isset($data->items) && is_array($data->items)) { + $items = array_map( + function (\stdClass $item): Item { + return $this->itemFactory->fromPayPalResponse($item); + }, + $data->items + ); + } + $payee = isset($data->payee) ? $this->payeeFactory->fromPayPalResponse($data->payee) : null; + $shipping = null; + try { + if (isset($data->shipping)) { + $shipping = $this->shippingFactory->fromPayPalResponse($data->shipping); + } + } catch (RuntimeException $error) { + } + $payments = null; + try { + if (isset($data->payments)) { + $payments = $this->paymentsFactory->fromPayPalResponse($data->payments); + } + } catch (RuntimeException $error) { + } + + $purchaseUnit = new PurchaseUnit( + $amount, + $items, + $shipping, + $data->reference_id, + $description, + $payee, + $customId, + $invoiceId, + $softDescriptor, + $payments + ); + return $purchaseUnit; + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/ShippingFactory.php b/modules.local/ppcp-api-client/src/Factory/ShippingFactory.php new file mode 100644 index 000000000..5ae81cf2e --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/ShippingFactory.php @@ -0,0 +1,63 @@ +addressFactory = $addressFactory; + } + + public function fromWcCustomer(\WC_Customer $customer): Shipping + { + // Replicates the Behavior of \WC_Order::get_formatted_shipping_full_name() + $fullName = sprintf( + // translators: %1$s is the first name and %2$s is the second name. wc translation. + _x('%1$s %2$s', 'full name', 'woocommerce'), + $customer->get_shipping_first_name(), + $customer->get_shipping_last_name() + ); + $address = $this->addressFactory->fromWcCustomer($customer); + return new Shipping( + $fullName, + $address + ); + } + + public function fromWcOrder(\WC_Order $order): Shipping + { + $fullName = $order->get_formatted_shipping_full_name(); + $address = $this->addressFactory->fromWcOrder($order); + return new Shipping( + $fullName, + $address + ); + } + + public function fromPayPalResponse(\stdClass $data): Shipping + { + if (! isset($data->name->full_name)) { + throw new RuntimeException( + __("No name was given for shipping.", "woocommerce-paypal-commerce-gateway") + ); + } + if (! isset($data->address)) { + throw new RuntimeException( + __("No address was given for shipping.", "woocommerce-paypal-commerce-gateway") + ); + } + $address = $this->addressFactory->fromPayPalRequest($data->address); + return new Shipping( + $data->name->full_name, + $address + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Factory/WebhookFactory.php b/modules.local/ppcp-api-client/src/Factory/WebhookFactory.php new file mode 100644 index 000000000..1159a453e --- /dev/null +++ b/modules.local/ppcp-api-client/src/Factory/WebhookFactory.php @@ -0,0 +1,56 @@ + $type]; + }, + $eventTypes + ); + return new Webhook( + $url, + $eventTypes + ); + } + + public function fromArray(array $data): Webhook + { + return $this->fromPayPalResponse((object) $data); + } + + public function fromPayPalResponse(\stdClass $data): Webhook + { + if (! isset($data->id)) { + throw new RuntimeException( + __("No id for webhook given.", "woocommerce-paypal-commerce-gateway") + ); + } + if (! isset($data->url)) { + throw new RuntimeException( + __("No URL for webhook given.", "woocommerce-paypal-commerce-gateway") + ); + } + if (! isset($data->event_types)) { + throw new RuntimeException( + __("No event types for webhook given.", "woocommerce-paypal-commerce-gateway") + ); + } + + return new Webhook( + (string) $data->url, + (array) $data->event_types, + (string) $data->id + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Helper/DccApplies.php b/modules.local/ppcp-api-client/src/Helper/DccApplies.php new file mode 100644 index 000000000..b33ba3bf8 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Helper/DccApplies.php @@ -0,0 +1,136 @@ + [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'BE' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'BG' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'CY' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'CZ' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'DK' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'EE' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'FI' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'FR' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'GR' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'HU' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'IT' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'LV' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'LI' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'LT' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'LU' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'MT' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'NL' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'NO' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'PL' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'PT' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'RO' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'SK' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'SI' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'ES' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'SE' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + 'US' => [ + 'AUD', 'CAD', 'EUR', 'GBP', 'JPY', 'USD', + ], + 'GB' => [ + 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', + 'HUF', 'JPY', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'USD', + ], + + ]; + + public function forCountryCurrency(): bool + { + $region = wc_get_base_location(); + $country = $region['country']; + $currency = get_woocommerce_currency(); + if (!in_array($country, array_keys($this->allowedCountryCurrencyMatrix), true)) { + return false; + } + $applies = in_array($currency, $this->allowedCountryCurrencyMatrix[$country], true); + return $applies; + } +} diff --git a/modules.local/ppcp-api-client/src/Helper/ErrorResponse.php b/modules.local/ppcp-api-client/src/Helper/ErrorResponse.php new file mode 100644 index 000000000..d0700f0c3 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Helper/ErrorResponse.php @@ -0,0 +1,114 @@ +settings = $settings; + } + + public function currentContext( + string $shippingPreference = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING + ): ApplicationContext { + + $brandName = $this->settings->has('brand_name') ? $this->settings->get('brand_name') : ''; + // Todo: Put user_locale in container as well? + $locale = str_replace('_', '-', get_user_locale()); + $landingpage = $this->settings->has('landing_page') ? + $this->settings->get('landing_page') : ApplicationContext::LANDING_PAGE_NO_PREFERENCE; + $context = new ApplicationContext( + (string) home_url(\WC_AJAX::get_endpoint(ReturnUrlEndpoint::ENDPOINT)), + (string) wc_get_checkout_url(), + (string) $brandName, + $locale, + (string) $landingpage, + $shippingPreference + ); + return $context; + } +} diff --git a/modules.local/ppcp-api-client/src/Repository/CartRepository.php b/modules.local/ppcp-api-client/src/Repository/CartRepository.php new file mode 100644 index 000000000..487699669 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Repository/CartRepository.php @@ -0,0 +1,29 @@ +factory = $factory; + } + + /** + * Returns all Pur of the Woocommerce cart. + * + * @return PurchaseUnit[] + */ + public function all(): array + { + $cart = WC()->cart ?? new \WC_Cart(); + return [$this->factory->fromWcCart($cart)]; + } +} diff --git a/modules.local/ppcp-api-client/src/Repository/PartnerReferralsData.php b/modules.local/ppcp-api-client/src/Repository/PartnerReferralsData.php new file mode 100644 index 000000000..5b18fa105 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Repository/PartnerReferralsData.php @@ -0,0 +1,81 @@ +merchantEmail = $merchantEmail; + $this->dccApplies = $dccApplies; + } + + public function nonce(): string + { + return 'a1233wtergfsdt4365tzrshgfbaewa36AGa1233wtergfsdt4365tzrshgfbaewa36AG'; + } + + public function data(): array + { + $data = $this->defaultData(); + return $data; + } + + private function defaultData(): array + { + + return [ + "email" => $this->merchantEmail, + "partner_config_override" => [ + "partner_logo_url" => "https://connect.woocommerce.com/images/woocommerce_logo.png", + "return_url" => admin_url( + 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' + ), + "return_url_description" => __( + 'Return to your shop.', + 'woocommerce-paypal-commerce-gateway' + ), + "show_add_credit_card" => true, + ], + "products" => [ + $this->dccApplies->forCountryCurrency() ? "PPCP" : "EXPRESS_CHECKOUT", + ], + "legal_consents" => [ + [ + "type" => "SHARE_DATA_CONSENT", + "granted" => true, + ], + ], + "operations" => [ + [ + "operation" => "API_INTEGRATION", + "api_integration_preference" => [ + "rest_api_integration" => [ + "integration_method" => "PAYPAL", + "integration_type" => "FIRST_PARTY", + "first_party_details" => [ + "features" => [ + "PAYMENT", + "FUTURE_PAYMENT", + "REFUND", + "ADVANCED_TRANSACTIONS_SEARCH", + ], + "seller_nonce" => $this->nonce(), + ], + ], + ], + ], + ], + ]; + } +} diff --git a/modules.local/ppcp-api-client/src/Repository/PayPalRequestIdRepository.php b/modules.local/ppcp-api-client/src/Repository/PayPalRequestIdRepository.php new file mode 100644 index 000000000..07b9c6c77 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Repository/PayPalRequestIdRepository.php @@ -0,0 +1,54 @@ +all(); + return isset($all[$orderId]) ? (string) $all[$orderId]['id'] : ''; + } + + public function getForOrder(Order $order): string + { + return $this->getForOrderId($order->id()); + } + + public function setForOrder(Order $order, string $requestId): bool + { + $all = $this->all(); + $all[$order->id()] = [ + 'id' => $requestId, + 'expiration' => time() + 10 * DAY_IN_SECONDS, + ]; + $all = $this->cleanup($all); + update_option(self::KEY, $all); + return true; + } + + private function all(): array + { + + return (array) get_option('ppcp-request-ids', []); + } + + private function cleanup(array $all): array + { + + foreach ($all as $orderId => $value) { + if (time() < $value['expiration']) { + continue; + } + unset($all[$orderId]); + } + return $all; + } +} diff --git a/modules.local/ppcp-api-client/src/Repository/PayeeRepository.php b/modules.local/ppcp-api-client/src/Repository/PayeeRepository.php new file mode 100644 index 000000000..ad0c69990 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Repository/PayeeRepository.php @@ -0,0 +1,28 @@ +merchantEmail = $merchantEmail; + $this->merchantId = $merchantId; + } + + public function payee(): Payee + { + return new Payee( + $this->merchantEmail, + $this->merchantId + ); + } +} diff --git a/modules.local/ppcp-api-client/src/Repository/PurchaseUnitRepositoryInterface.php b/modules.local/ppcp-api-client/src/Repository/PurchaseUnitRepositoryInterface.php new file mode 100644 index 000000000..c52679b78 --- /dev/null +++ b/modules.local/ppcp-api-client/src/Repository/PurchaseUnitRepositoryInterface.php @@ -0,0 +1,16 @@ +expects('get') + ->andReturn('{"access_token":"abc","expires_in":100, "created":100}'); + $cache + ->expects('set'); + $host = 'https://example.com'; + $key = 'key'; + $secret = 'secret'; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + + $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger); + + expect('trailingslashit') + ->with($host) + ->andReturn($host . '/'); + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($json, $key, $secret, $host) { + if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Basic ' . base64_encode($key . ':' . $secret)) { + return false; + } + + return [ + 'body' => $json, + ]; + } + ); + expect('is_wp_error') + ->andReturn(false); + expect('wp_remote_retrieve_response_code') + ->andReturn(200); + + $token = $bearer->bearer(); + $this->assertEquals("abc", $token->token()); + $this->assertTrue($token->isValid()); + } + + public function testNoTokenCached() + { + $json = '{"access_token":"abc","expires_in":100, "created":' . time() . '}'; + $cache = Mockery::mock(CacheInterface::class); + $cache + ->expects('get') + ->andReturn(''); + $cache + ->expects('set'); + $host = 'https://example.com'; + $key = 'key'; + $secret = 'secret'; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + + $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger); + + expect('trailingslashit') + ->with($host) + ->andReturn($host . '/'); + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($json, $key, $secret, $host) { + if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Basic ' . base64_encode($key . ':' . $secret)) { + return false; + } + + return [ + 'body' => $json, + ]; + } + ); + expect('is_wp_error') + ->andReturn(false); + expect('wp_remote_retrieve_response_code') + ->andReturn(200); + + $token = $bearer->bearer(); + $this->assertEquals("abc", $token->token()); + $this->assertTrue($token->isValid()); + } + + public function testCachedTokenIsStillValid() + { + $json = '{"access_token":"abc","expires_in":100, "created":' . time() . '}'; + $cache = Mockery::mock(CacheInterface::class); + $cache + ->expects('get') + ->andReturn($json); + $host = 'https://example.com'; + $key = 'key'; + $secret = 'secret'; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + + $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger); + + $token = $bearer->bearer(); + $this->assertEquals("abc", $token->token()); + $this->assertTrue($token->isValid()); + } + + public function testExceptionThrownOnError() + { + $json = '{"access_token":"abc","expires_in":100, "created":' . time() . '}'; + $cache = Mockery::mock(CacheInterface::class); + $cache + ->expects('get') + ->andReturn(''); + $host = 'https://example.com'; + $key = 'key'; + $secret = 'secret'; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + + $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger); + + expect('trailingslashit') + ->with($host) + ->andReturn($host . '/'); + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($json, $key, $secret, $host) { + if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Basic ' . base64_encode($key . ':' . $secret)) { + return false; + } + + return [ + 'body' => $json, + ]; + } + ); + expect('is_wp_error') + ->andReturn(true); + + $this->expectException(RuntimeException::class); + $bearer->bearer(); + } + + public function testExceptionThrownBecauseOfHttpStatusCode() + { + $json = '{"access_token":"abc","expires_in":100, "created":' . time() . '}'; + $cache = Mockery::mock(CacheInterface::class); + $cache + ->expects('get') + ->andReturn(''); + $host = 'https://example.com'; + $key = 'key'; + $secret = 'secret'; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + + $bearer = new PayPalBearer($cache, $host, $key, $secret, $logger); + + expect('trailingslashit') + ->with($host) + ->andReturn($host . '/'); + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($json, $key, $secret, $host) { + if ($url !== $host . '/v1/oauth2/token?grant_type=client_credentials') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Basic ' . base64_encode($key . ':' . $secret)) { + return false; + } + + return [ + 'body' => $json, + ]; + } + ); + expect('is_wp_error') + ->andReturn(false); + expect('wp_remote_retrieve_response_code') + ->andReturn(500); + + $this->expectException(RuntimeException::class); + $bearer->bearer(); + } +} diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php new file mode 100644 index 000000000..084e3a822 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php @@ -0,0 +1,1051 @@ +expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $order = Mockery::mock(Order::class); + $orderFactory + ->expects('fromPayPalResponse') + ->andReturnUsing(function (\stdClass $object) use ($order) : ?Order { + return ($object->is_correct) ? $order : null; + }); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrderId')->with($orderId)->andReturn('uniqueRequestId'); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $rawResponse = ['body' => '{"is_correct":true}']; + expect('wp_remote_get') + ->andReturnUsing(function ($url, $args) use ($rawResponse, $host, $orderId) { + if ($url !== $host . 'v2/checkout/orders/' . $orderId) { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + return $rawResponse; + }); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200); + + $result = $testee->order($orderId); + $this->assertEquals($order, $result); + } + + public function testOrderResponseIsWpError() + { + $host = 'https://example.com/'; + $orderId = 'id'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrderId')->with($orderId)->andReturn('uniqueRequestId'); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $rawResponse = ['body' => '{"is_correct":true}']; + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(true); + + $this->expectException(RuntimeException::class); + $testee->order($orderId); + } + + public function testOrderResponseIsNot200() + { + $host = 'https://example.com/'; + $orderId = 'id'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + $rawResponse = ['body' => '{"some_error":true}']; + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrderId')->with($orderId)->andReturn('uniqueRequestId'); + + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); + + $this->expectException(RuntimeException::class); + $testee->order($orderId); + } + + public function testCaptureDefault() + { + $orderId = 'id'; + $orderToCaptureStatus = Mockery::mock(OrderStatus::class); + $orderToCaptureStatus->expects('is')->with('COMPLETED')->andReturn(false); + $orderToCapture = Mockery::mock(Order::class); + $orderToCapture->expects('status')->andReturn($orderToCaptureStatus); + $orderToCapture->expects('id')->andReturn($orderId); + + $rawResponse = ['body' => '{"is_correct":true}']; + $expectedOrder = Mockery::mock(Order::class); + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $orderFactory + ->expects('fromPayPalResponse') + ->andReturnUsing( + function ($json) use ($expectedOrder) { + if ($json->is_correct) { + return $expectedOrder; + } + return Mockery::mock(Order::class); + } + ); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrder')->with($orderToCapture)->andReturn('uniqueRequestId'); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($rawResponse, $host, $orderId) { + if ($url !== $host . 'v2/checkout/orders/' . $orderId . '/capture') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + if ($args['headers']['Prefer'] !== 'return=representation') { + return false; + } + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(201); + + $result = $testee->capture($orderToCapture); + $this->assertEquals($expectedOrder, $result); + } + + public function testCaptureAlreadyCompletedOrder() + { + $orderToCaptureStatus = Mockery::mock(OrderStatus::class); + $orderToCaptureStatus->expects('is')->with('COMPLETED')->andReturn(true); + $orderToCapture = Mockery::mock(Order::class); + $orderToCapture->expects('status')->andReturn($orderToCaptureStatus); + + $host = 'https://example.com/'; + $bearer = Mockery::mock(Bearer::class); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $result = $testee->capture($orderToCapture); + $this->assertEquals($orderToCapture, $result); + } + + public function testCaptureIsWpError() + { + $orderId = 'id'; + $orderToCaptureStatus = Mockery::mock(OrderStatus::class); + $orderToCaptureStatus->expects('is')->with('COMPLETED')->andReturn(false); + $orderToCapture = Mockery::mock(Order::class); + $orderToCapture->expects('status')->andReturn($orderToCaptureStatus); + $orderToCapture->expects('id')->andReturn($orderId); + + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrder')->with($orderToCapture)->andReturn('uniqueRequestId'); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $rawResponse = ['body' => '{"is_error":true}']; + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(true); + $this->expectException(RuntimeException::class); + $testee->capture($orderToCapture); + } + + public function testCaptureIsNot201() + { + $orderId = 'id'; + $orderToCaptureStatus = Mockery::mock(OrderStatus::class); + $orderToCaptureStatus->expects('is')->with('COMPLETED')->andReturn(false); + $orderToCapture = Mockery::mock(Order::class); + $orderToCapture->expects('status')->andReturn($orderToCaptureStatus); + $orderToCapture->expects('id')->andReturn($orderId); + + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrder')->with($orderToCapture)->andReturn('uniqueRequestId'); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $rawResponse = ['body' => '{"some_error":true}']; + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); + $this->expectException(RuntimeException::class); + $testee->capture($orderToCapture); + } + + public function testCaptureIsNot201ButAlreadyCaptured() + { + $orderId = 'id'; + $orderToCaptureStatus = Mockery::mock(OrderStatus::class); + $orderToCaptureStatus->expects('is')->with('COMPLETED')->andReturn(false); + $orderToCapture = Mockery::mock(Order::class); + $orderToCapture->expects('status')->andReturn($orderToCaptureStatus); + $orderToCapture->shouldReceive('id')->andReturn($orderId); + + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrder')->with($orderToCapture)->andReturn('uniqueRequestId'); + $testee = Mockery::mock( + OrderEndpoint::class, + [ + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository, + ] + )->makePartial(); + $orderToExpect = Mockery::mock(Order::class); + $testee->expects('order')->with($orderId)->andReturn($orderToExpect); + + $rawResponse = ['body' => '{"some_error": "' . ErrorResponse::ORDER_ALREADY_CAPTURED . '"}']; + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); + $result = $testee->capture($orderToCapture); + $this->assertEquals($orderToExpect, $result); + } + + public function testPatchOrderWithDefault() + { + $orderId = 'id'; + $orderToUpdate = Mockery::mock(Order::class); + $orderToUpdate + ->shouldReceive('id') + ->andReturn($orderId); + $orderToCompare = Mockery::mock(Order::class); + + $rawResponse = ['body' => '{"is_correct":true}']; + $expectedOrder = Mockery::mock(Order::class); + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patches = ['patch-1', 'patch-2']; + $patchCollection = Mockery::mock(PatchCollection::class); + $patchCollection + ->expects('patches') + ->andReturn($patches); + $patchCollection + ->expects('toArray') + ->andReturn($patches); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $patchCollectionFactory + ->expects('fromOrders') + ->with($orderToUpdate, $orderToCompare) + ->andReturn($patchCollection); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrder')->with($orderToUpdate)->andReturn('uniqueRequestId'); + $testee = Mockery::mock( + OrderEndpoint::class, + [ + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository, + ] + )->makePartial(); + $testee + ->expects('order') + ->with($orderId) + ->andReturn($expectedOrder); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($host, $orderId, $rawResponse) { + if ($url !== $host . 'v2/checkout/orders/' . $orderId) { + return false; + } + if ($args['method'] !== 'PATCH') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + if ($args['headers']['Prefer'] !== 'return=representation') { + return false; + } + if ($args['headers']['PayPal-Request-Id'] !== 'uniqueRequestId') { + return false; + } + $body = json_decode($args['body']); + if (! is_array($body) || $body[0] !== 'patch-1' || $body[1] !== 'patch-2') { + return false; + } + + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(204); + $result = $testee->patchOrderWith($orderToUpdate, $orderToCompare); + $this->assertEquals($expectedOrder, $result); + } + + public function testPatchOrderWithIsNot204() + { + $orderId = 'id'; + $orderToUpdate = Mockery::mock(Order::class); + $orderToUpdate + ->shouldReceive('id') + ->andReturn($orderId); + $orderToCompare = Mockery::mock(Order::class); + + $rawResponse = ['body' => '{"has_error":true}']; + $expectedOrder = Mockery::mock(Order::class); + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patches = ['patch-1', 'patch-2']; + $patchCollection = Mockery::mock(PatchCollection::class); + $patchCollection + ->expects('patches') + ->andReturn($patches); + $patchCollection + ->expects('toArray') + ->andReturn($patches); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $patchCollectionFactory + ->expects('fromOrders') + ->with($orderToUpdate, $orderToCompare) + ->andReturn($patchCollection); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrder')->with($orderToUpdate)->andReturn('uniqueRequestId'); + + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($host, $orderId, $rawResponse) { + if ($url !== $host . 'v2/checkout/orders/' . $orderId) { + return false; + } + if ($args['method'] !== 'PATCH') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + if ($args['headers']['Prefer'] !== 'return=representation') { + return false; + } + if ($args['headers']['PayPal-Request-Id'] !== 'uniqueRequestId') { + return false; + } + $body = json_decode($args['body']); + if (! is_array($body) || $body[0] !== 'patch-1' || $body[1] !== 'patch-2') { + return false; + } + + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); + $this->expectException(RuntimeException::class); + $testee->patchOrderWith($orderToUpdate, $orderToCompare); + } + + public function testPatchOrderWithIsWpError() + { + $orderId = 'id'; + $orderToUpdate = Mockery::mock(Order::class); + $orderToUpdate + ->shouldReceive('id') + ->andReturn($orderId); + $orderToCompare = Mockery::mock(Order::class); + + $rawResponse = ['body' => '{"is_correct":true}']; + $expectedOrder = Mockery::mock(Order::class); + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patches = ['patch-1', 'patch-2']; + $patchCollection = Mockery::mock(PatchCollection::class); + $patchCollection + ->expects('patches') + ->andReturn($patches); + $patchCollection + ->expects('toArray') + ->andReturn($patches); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $patchCollectionFactory + ->expects('fromOrders') + ->with($orderToUpdate, $orderToCompare) + ->andReturn($patchCollection); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('getForOrder')->with($orderToUpdate)->andReturn('uniqueRequestId'); + $testee = Mockery::mock( + OrderEndpoint::class, + [ + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ] + )->makePartial(); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($host, $orderId, $rawResponse) { + if ($url !== $host . 'v2/checkout/orders/' . $orderId) { + return false; + } + if ($args['method'] !== 'PATCH') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + if ($args['headers']['Prefer'] !== 'return=representation') { + return false; + } + if ($args['headers']['PayPal-Request-Id'] !== 'uniqueRequestId') { + return false; + } + $body = json_decode($args['body']); + if (! is_array($body) || $body[0] !== 'patch-1' || $body[1] !== 'patch-2') { + return false; + } + + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(true); + $this->expectException(RuntimeException::class); + $testee->patchOrderWith($orderToUpdate, $orderToCompare); + } + + public function testPatchOrderWithNoPatches() + { + $orderToUpdate = Mockery::mock(Order::class); + $orderToCompare = Mockery::mock(Order::class); + + $host = 'https://example.com/'; + $bearer = Mockery::mock(Bearer::class); + $orderFactory = Mockery::mock(OrderFactory::class); + $patches = []; + $patchCollection = Mockery::mock(PatchCollection::class); + $patchCollection + ->expects('patches') + ->andReturn($patches); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $patchCollectionFactory + ->expects('fromOrders') + ->with($orderToUpdate, $orderToCompare) + ->andReturn($patchCollection); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $result = $testee->patchOrderWith($orderToUpdate, $orderToCompare); + $this->assertEquals($orderToUpdate, $result); + } + + public function testCreateForPurchaseUnitsDefault() + { + $rawResponse = ['body' => '{"success":true}']; + $host = 'https://example.com/'; + $bearer = Mockery::mock(Bearer::class); + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer + ->expects('bearer') + ->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $expectedOrder = Mockery::mock(Order::class); + $orderFactory + ->expects('fromPayPalResponse') + ->andReturnUsing(function ($json) use ($expectedOrder, $rawResponse) { + if (! $json->success) { + return Mockery::mock(Order::class); + } + return $expectedOrder; + }); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContext = Mockery::mock(ApplicationContext::class); + $applicationContext + ->expects('toArray') + ->andReturn(['applicationContext']); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $applicationContextRepository + ->expects('currentContext') + ->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING)) + ->andReturn($applicationContext); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('setForOrder')->andReturnUsing(function ($order, $id) use ($expectedOrder) : bool { + if ($order !== $expectedOrder) { + return false; + } + + return strpos($id, 'ppcp') !== false; + }); + + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $purchaseUnit = Mockery::mock(PurchaseUnit::class, ['containsPhysicalGoodsItems' => false]); + $purchaseUnit + ->expects('toArray') + ->andReturn(['singlePurchaseUnit']); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($rawResponse, $host) { + if ($url !== $host . 'v2/checkout/orders') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + if ($args['headers']['Prefer'] !== 'return=representation') { + return false; + } + $body = json_decode($args['body'], true); + if ($body['intent'] !== 'CAPTURE') { + return false; + } + if ($body['purchase_units'][0][0] !== 'singlePurchaseUnit') { + return false; + } + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(201); + $result = $testee->createForPurchaseUnits([$purchaseUnit]); + $this->assertEquals($expectedOrder, $result); + } + + public function testCreateForPurchaseUnitsWithPayer() + { + $rawResponse = ['body' => '{"success":true}']; + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer') + ->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $expectedOrder = Mockery::mock(Order::class); + $orderFactory + ->expects('fromPayPalResponse') + ->andReturnUsing(function ($json) use ($expectedOrder, $rawResponse) { + if (! $json->success) { + return Mockery::mock(Order::class); + } + return $expectedOrder; + }); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + $applicationContext = Mockery::mock(ApplicationContext::class); + $applicationContext + ->expects('toArray') + ->andReturn(['applicationContext']); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $applicationContextRepository + ->expects('currentContext') + ->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE)) + ->andReturn($applicationContext); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $paypalRequestIdRepository + ->expects('setForOrder')->andReturnUsing(function ($order, $id) use ($expectedOrder) : bool { + if ($order !== $expectedOrder) { + return false; + } + + return strpos($id, 'ppcp') !== false; + }); + + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $purchaseUnit = Mockery::mock(PurchaseUnit::class, ['containsPhysicalGoodsItems' => true]); + $purchaseUnit + ->expects('toArray') + ->andReturn(['singlePurchaseUnit']); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($rawResponse, $host) { + $body = json_decode($args['body'], true); + if ($args['method'] !== 'POST') { + return false; + } + if (! isset($body['payer']) || $body['payer'][0] !== 'payer') { + return false; + } + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(201); + + $payer = Mockery::mock(Payer::class); + $payer->expects('toArray')->andReturn(['payer']); + $result = $testee->createForPurchaseUnits([$purchaseUnit], $payer); + $this->assertEquals($expectedOrder, $result); + } + + public function testCreateForPurchaseUnitsIsWpError() + { + $rawResponse = ['body' => '{"success":true}']; + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer') + ->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + $applicationContext = Mockery::mock(ApplicationContext::class); + $applicationContext + ->expects('toArray') + ->andReturn(['applicationContext']); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $applicationContextRepository + ->expects('currentContext') + ->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING)) + ->andReturn($applicationContext); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $purchaseUnit = Mockery::mock(PurchaseUnit::class, ['containsPhysicalGoodsItems' => false]); + $purchaseUnit + ->expects('toArray') + ->andReturn(['singlePurchaseUnit']); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($rawResponse, $host) { + if ($url !== $host . 'v2/checkout/orders') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + if ($args['headers']['Prefer'] !== 'return=representation') { + return false; + } + $body = json_decode($args['body'], true); + if ($body['intent'] !== 'CAPTURE') { + return false; + } + if ($body['purchase_units'][0][0] !== 'singlePurchaseUnit') { + return false; + } + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(true); + $this->expectException(RuntimeException::class); + $testee->createForPurchaseUnits([$purchaseUnit]); + } + + public function testCreateForPurchaseUnitsIsNot201() + { + $rawResponse = ['body' => '{"has_error":true}']; + $host = 'https://example.com/'; + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer') + ->andReturn($token); + $orderFactory = Mockery::mock(OrderFactory::class); + $patchCollectionFactory = Mockery::mock(PatchCollectionFactory::class); + $intent = 'CAPTURE'; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + $applicationContext = Mockery::mock(ApplicationContext::class); + $applicationContext + ->expects('toArray') + ->andReturn(['applicationContext']); + $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); + $applicationContextRepository + ->expects('currentContext') + ->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE)) + ->andReturn($applicationContext); + $paypalRequestIdRepository = Mockery::mock(PayPalRequestIdRepository::class); + $testee = new OrderEndpoint( + $host, + $bearer, + $orderFactory, + $patchCollectionFactory, + $intent, + $logger, + $applicationContextRepository, + $paypalRequestIdRepository + ); + + $purchaseUnit = Mockery::mock(PurchaseUnit::class, ['containsPhysicalGoodsItems' => true]); + $purchaseUnit + ->expects('toArray') + ->andReturn(['singlePurchaseUnit']); + + expect('wp_remote_get') + ->andReturnUsing( + function ($url, $args) use ($rawResponse, $host) { + if ($url !== $host . 'v2/checkout/orders') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + if ($args['headers']['Prefer'] !== 'return=representation') { + return false; + } + $body = json_decode($args['body'], true); + if ($body['intent'] !== 'CAPTURE') { + return false; + } + if ($body['purchase_units'][0][0] !== 'singlePurchaseUnit') { + return false; + } + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); + $this->expectException(RuntimeException::class); + $testee->createForPurchaseUnits([$purchaseUnit]); + } +} diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderTest.php new file mode 100644 index 000000000..b9fd71318 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderTest.php @@ -0,0 +1,100 @@ +expects('toArray')->andReturn([1]); + $status = Mockery::mock(OrderStatus::class); + $status->expects('name')->andReturn('CREATED'); + $payer = Mockery::mock(Payer::class); + $payer + ->expects('toArray')->andReturn(['payer']); + $intent = 'AUTHORIZE'; + $applicationContext = Mockery::mock(ApplicationContext::class); + $applicationContext + ->expects('toArray') + ->andReturn(['applicationContext']); + $paymentSource = Mockery::mock(PaymentSource::class); + $paymentSource + ->expects('toArray') + ->andReturn(['paymentSource']); + + $testee = new Order( + $id, + [$unit], + $status, + $applicationContext, + $paymentSource, + $payer, + $intent, + $createTime, + $updateTime + ); + + $this->assertEquals($id, $testee->id()); + $this->assertEquals($createTime, $testee->createTime()); + $this->assertEquals($updateTime, $testee->updateTime()); + $this->assertEquals([$unit], $testee->purchaseUnits()); + $this->assertEquals($payer, $testee->payer()); + $this->assertEquals($intent, $testee->intent()); + $this->assertEquals($status, $testee->status()); + + $expected = [ + 'id' => $id, + 'intent' => $intent, + 'status' => 'CREATED', + 'purchase_units' => [ + [1], + ], + 'create_time' => $createTime->format(\DateTimeInterface::ISO8601), + 'update_time' => $updateTime->format(\DateTimeInterface::ISO8601), + 'payer' => ['payer'], + 'application_context' => ['applicationContext'], + 'payment_source' => ['paymentSource'] + ]; + $this->assertEquals($expected, $testee->toArray()); + } + + public function testOrderNoDatesOrPayer() + { + $id = 'id'; + $unit = Mockery::mock(PurchaseUnit::class); + $unit->expects('toArray')->andReturn([1]); + $status = Mockery::mock(OrderStatus::class); + $status->expects('name')->andReturn('CREATED'); + + $testee = new Order( + $id, + [$unit], + $status + ); + + $this->assertEquals(null, $testee->createTime()); + $this->assertEquals(null, $testee->updateTime()); + $this->assertEquals(null, $testee->payer()); + $this->assertEquals('CAPTURE', $testee->intent()); + + $array = $testee->toArray(); + $this->assertFalse(array_key_exists('payer', $array)); + $this->assertFalse(array_key_exists('create_time', $array)); + $this->assertFalse(array_key_exists('update_time', $array)); + } +} diff --git a/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php new file mode 100644 index 000000000..5e98c2f76 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Endpoint/PaymentsEndpointTest.php @@ -0,0 +1,259 @@ +expects('token')->andReturn('bearer'); + $bearer + ->expects('bearer')->andReturn($token); + + $authorization = Mockery::mock(Authorization::class); + $authorizationFactory = Mockery::mock(AuthorizationFactory::class); + $authorizationFactory + ->expects('fromPayPalRequest') + ->andReturn($authorization); + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + + $rawResponse = ['body' => '{"is_correct":true}']; + + $testee = new PaymentsEndpoint( + $host, + $bearer, + $authorizationFactory, + $logger + ); + + expect('wp_remote_get')->andReturnUsing( + function ($url, $args) use ($rawResponse, $host, $authorizationId) { + if ($url !== $host . 'v2/payments/authorizations/' . $authorizationId) { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200); + + $result = $testee->authorization($authorizationId); + $this->assertEquals($authorization, $result); + } + + public function testAuthorizationWpError() + { + $host = 'https://example.com/'; + $authorizationId = 'somekindofid'; + + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer->expects('bearer')->andReturn($token); + + $authorizationFactory = Mockery::mock(AuthorizationFactory::class); + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + + $rawResponse = ['body' => '{"is_correct":true}']; + + $testee = new PaymentsEndpoint( + $host, + $bearer, + $authorizationFactory, + $logger + ); + + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(true); + + $this->expectException(RuntimeException::class); + $testee->authorization($authorizationId); + } + + public function testAuthorizationIsNot200() + { + $host = 'https://example.com/'; + $authorizationId = 'somekindofid'; + + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer->expects('bearer')->andReturn($token); + + $authorizationFactory = Mockery::mock(AuthorizationFactory::class); + + $rawResponse = ['body' => '{"some_error":true}']; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldReceive('log'); + + $testee = new PaymentsEndpoint( + $host, + $bearer, + $authorizationFactory, + $logger + ); + + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); + + $this->expectException(RuntimeException::class); + $testee->authorization($authorizationId); + } + + public function testCaptureDefault() + { + $host = 'https://example.com/'; + $authorizationId = 'somekindofid'; + + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer + ->expects('bearer')->andReturn($token); + + $authorization = Mockery::mock(Authorization::class); + $authorizationFactory = Mockery::mock(AuthorizationFactory::class); + $authorizationFactory + ->expects('fromPayPalRequest') + ->andReturn($authorization); + + + $logger = Mockery::mock(LoggerInterface::class); + $logger->shouldNotReceive('log'); + + $rawResponse = ['body' => '{"is_correct":true}']; + + $testee = new PaymentsEndpoint( + $host, + $bearer, + $authorizationFactory, + $logger + ); + + expect('wp_remote_get')->andReturnUsing( + function ($url, $args) use ($rawResponse, $host, $authorizationId) { + if ($url !== $host . 'v2/payments/authorizations/' . $authorizationId . '/capture') { + return false; + } + if ($args['method'] !== 'POST') { + return false; + } + if ($args['headers']['Authorization'] !== 'Bearer bearer') { + return false; + } + if ($args['headers']['Content-Type'] !== 'application/json') { + return false; + } + + return $rawResponse; + } + ); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(201); + + $result = $testee->capture($authorizationId); + $this->assertEquals($authorization, $result); + } + + public function testCaptureIsWpError() + { + $host = 'https://example.com/'; + $authorizationId = 'somekindofid'; + + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer->expects('bearer')->andReturn($token); + + $authorizationFactory = Mockery::mock(AuthorizationFactory::class); + + $logger = Mockery::mock(LoggerInterface::class); + $logger->expects('log'); + + $rawResponse = ['body' => '{"is_correct":true}']; + + $testee = new PaymentsEndpoint( + $host, + $bearer, + $authorizationFactory, + $logger + ); + + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(true); + + $this->expectException(RuntimeException::class); + $testee->capture($authorizationId); + } + + public function testAuthorizationIsNot201() + { + $host = 'https://example.com/'; + $authorizationId = 'somekindofid'; + + $token = Mockery::mock(Token::class); + $token + ->expects('token')->andReturn('bearer'); + $bearer = Mockery::mock(Bearer::class); + $bearer->expects('bearer')->andReturn($token); + + $authorizationFactory = Mockery::mock(AuthorizationFactory::class); + + $rawResponse = ['body' => '{"some_error":true}']; + + $logger = Mockery::mock(LoggerInterface::class); + $logger->expects('log'); + + $testee = new PaymentsEndpoint( + $host, + $bearer, + $authorizationFactory, + $logger + ); + + expect('wp_remote_get')->andReturn($rawResponse); + expect('is_wp_error')->with($rawResponse)->andReturn(false); + expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(500); + + $this->expectException(RuntimeException::class); + $testee->capture($authorizationId); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/AddressTest.php b/tests/PHPUnit/ApiClient/Entity/AddressTest.php new file mode 100644 index 000000000..3aa2a7380 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/AddressTest.php @@ -0,0 +1,53 @@ +assertEquals('countryCode', $testee->countryCode()); + $this->assertEquals('addressLine1', $testee->addressLine1()); + $this->assertEquals('addressLine2', $testee->addressLine2()); + $this->assertEquals('adminArea1', $testee->adminArea1()); + $this->assertEquals('adminArea2', $testee->adminArea2()); + $this->assertEquals('postalCode', $testee->postalCode()); + } + + public function testToArray() + { + $testee = new Address( + 'countryCode', + 'addressLine1', + 'addressLine2', + 'adminArea1', + 'adminArea2', + 'postalCode' + ); + + $expected = [ + 'country_code' => 'countryCode', + 'address_line_1' => 'addressLine1', + 'address_line_2' => 'addressLine2', + 'admin_area_1' => 'adminArea1', + 'admin_area_2' => 'adminArea2', + 'postal_code' => 'postalCode', + ]; + + $actual = $testee->toArray(); + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/AmountBreakdownTest.php b/tests/PHPUnit/ApiClient/Entity/AmountBreakdownTest.php new file mode 100644 index 000000000..3911e2d75 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/AmountBreakdownTest.php @@ -0,0 +1,123 @@ +expects('toArray')->andReturn(['itemTotal']); + $shipping = Mockery::mock(Money::class); + $shipping + ->expects('toArray')->andReturn(['shipping']); + $taxTotal = Mockery::mock(Money::class); + $taxTotal + ->expects('toArray')->andReturn(['taxTotal']); + $handling = Mockery::mock(Money::class); + $handling + ->expects('toArray')->andReturn(['handling']); + $insurance = Mockery::mock(Money::class); + $insurance + ->expects('toArray')->andReturn(['insurance']); + $shippingDiscount = Mockery::mock(Money::class); + $shippingDiscount + ->expects('toArray')->andReturn(['shippingDiscount']); + $discount = Mockery::mock(Money::class); + $discount + ->expects('toArray')->andReturn(['discount']); + $testee = new AmountBreakdown( + $itemTotal, + $shipping, + $taxTotal, + $handling, + $insurance, + $shippingDiscount, + $discount + ); + + $this->assertEquals($itemTotal, $testee->itemTotal()); + $this->assertEquals($shipping, $testee->shipping()); + $this->assertEquals($taxTotal, $testee->taxTotal()); + $this->assertEquals($handling, $testee->handling()); + $this->assertEquals($insurance, $testee->insurance()); + $this->assertEquals($shippingDiscount, $testee->shippingDiscount()); + $this->assertEquals($discount, $testee->discount()); + + $expected = [ + 'item_total' => ['itemTotal'], + 'shipping' => ['shipping'], + 'tax_total' => ['taxTotal'], + 'handling' => ['handling'], + 'insurance' => ['insurance'], + 'shipping_discount' => ['shippingDiscount'], + 'discount' => ['discount'], + ]; + + $this->assertEquals($expected, $testee->toArray()); + } + + /** + * @dataProvider dataDropArrayKeyIfNoValueGiven + */ + public function testDropArrayKeyIfNoValueGiven($keyMissing, $methodName) + { + $itemTotal = Mockery::mock(Money::class); + $itemTotal + ->shouldReceive('toArray')->zeroOrMoreTimes()->andReturn(['itemTotal']); + $shipping = Mockery::mock(Money::class); + $shipping + ->shouldReceive('toArray')->zeroOrMoreTimes()->andReturn(['shipping']); + $taxTotal = Mockery::mock(Money::class); + $taxTotal + ->shouldReceive('toArray')->zeroOrMoreTimes()->andReturn(['taxTotal']); + $handling = Mockery::mock(Money::class); + $handling + ->shouldReceive('toArray')->zeroOrMoreTimes()->andReturn(['handling']); + $insurance = Mockery::mock(Money::class); + $insurance + ->shouldReceive('toArray')->zeroOrMoreTimes()->andReturn(['insurance']); + $shippingDiscount = Mockery::mock(Money::class); + $shippingDiscount + ->shouldReceive('toArray')->zeroOrMoreTimes()->andReturn(['shippingDiscount']); + $discount = Mockery::mock(Money::class); + $discount + ->shouldReceive('toArray')->zeroOrMoreTimes()->andReturn(['discount']); + + $items = [ + 'item_total' => $itemTotal, + 'shipping' => $shipping, + 'tax_total' => $taxTotal, + 'handling' => $handling, + 'insurance' => $insurance, + 'shipping_discount' => $shippingDiscount, + 'discount' => $discount, + ]; + $items[$keyMissing] = null; + + $testee = new AmountBreakdown(...array_values($items)); + $array = $testee->toArray(); + $result = ! array_key_exists($keyMissing, $array); + $this->assertTrue($result); + $this->assertNull($testee->{$methodName}(), "$methodName should return null"); + } + + public function dataDropArrayKeyIfNoValueGiven() : array + { + return [ + ['item_total', 'itemTotal'], + ['shipping', 'shipping'], + ['tax_total', 'taxTotal'], + ['handling', 'handling'], + ['insurance', 'insurance'], + ['shipping_discount', 'shippingDiscount'], + ['discount', 'discount'], + ]; + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/AmountTest.php b/tests/PHPUnit/ApiClient/Entity/AmountTest.php new file mode 100644 index 000000000..e8c592bf9 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/AmountTest.php @@ -0,0 +1,57 @@ +shouldReceive('currencyCode')->andReturn('currencyCode'); + $money->shouldReceive('value')->andReturn(1.10); + $testee = new Amount($money); + + $this->assertEquals('currencyCode', $testee->currencyCode()); + $this->assertEquals(1.10, $testee->value()); + } + + public function testBreakdownIsNull() + { + $money = Mockery::mock(Money::class); + $money->shouldReceive('currencyCode')->andReturn('currencyCode'); + $money->shouldReceive('value')->andReturn(1.10); + $testee = new Amount($money); + + $this->assertNull($testee->breakdown()); + + $expectedArray = [ + 'currency_code' => 'currencyCode', + 'value' => 1.10, + ]; + $this->assertEquals($expectedArray, $testee->toArray()); + } + + public function testBreakdown() + { + $money = Mockery::mock(Money::class); + $money->shouldReceive('currencyCode')->andReturn('currencyCode'); + $money->shouldReceive('value')->andReturn(1.10); + $breakdown = Mockery::mock(AmountBreakdown::class); + $breakdown->shouldReceive('toArray')->andReturn([1]); + $testee = new Amount($money, $breakdown); + + $this->assertEquals($breakdown, $testee->breakdown()); + + $expectedArray = [ + 'currency_code' => 'currencyCode', + 'value' => 1.10, + 'breakdown' => [1], + ]; + $this->assertEquals($expectedArray, $testee->toArray()); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/AuthorizationStatusTest.php b/tests/PHPUnit/ApiClient/Entity/AuthorizationStatusTest.php new file mode 100644 index 000000000..b8870e97e --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/AuthorizationStatusTest.php @@ -0,0 +1,51 @@ +assertEquals($authorizationStatus->name(), $status); + } + + public function testInvalidStatusProvided() + { + $this->expectException(RuntimeException::class); + + new AuthorizationStatus('invalid'); + } + + public function testStatusComparision() + { + $authorizationStatus = new AuthorizationStatus('CREATED'); + + $this->assertTrue($authorizationStatus->is('CREATED')); + $this->assertFalse($authorizationStatus->is('NOT_CREATED')); + } + + public function statusDataProvider(): array + { + return [ + ['INTERNAL'], + ['CREATED'], + ['CAPTURED'], + ['DENIED'], + ['EXPIRED'], + ['PARTIALLY_CAPTURED'], + ['VOIDED'], + ['PENDING'], + ]; + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php b/tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php new file mode 100644 index 000000000..bbada5e45 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/AuthorizationTest.php @@ -0,0 +1,33 @@ +assertEquals('foo', $testee->id()); + $this->assertEquals($authorizationStatus, $testee->status()); + } + + public function testToArray() + { + $authorizationStatus = \Mockery::mock(AuthorizationStatus::class); + $authorizationStatus->expects('name')->andReturn('CAPTURED'); + + $testee = new Authorization('foo', $authorizationStatus); + + $expected = [ + 'id' => 'foo', + 'status' => 'CAPTURED', + ]; + $this->assertEquals($expected, $testee->toArray()); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/ItemTest.php b/tests/PHPUnit/ApiClient/Entity/ItemTest.php new file mode 100644 index 000000000..cc4d2e6b9 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/ItemTest.php @@ -0,0 +1,84 @@ +assertEquals('name', $testee->name()); + $this->assertEquals($unitAmount, $testee->unitAmount()); + $this->assertEquals(1, $testee->quantity()); + $this->assertEquals('description', $testee->description()); + $this->assertEquals($tax, $testee->tax()); + $this->assertEquals('sku', $testee->sku()); + $this->assertEquals('PHYSICAL_GOODS', $testee->category()); + } + + public function testDigitalGoodsCategory() + { + $unitAmount = Mockery::mock(Money::class); + $tax = Mockery::mock(Money::class); + $testee = new Item( + 'name', + $unitAmount, + 1, + 'description', + $tax, + 'sku', + 'DIGITAL_GOODS' + ); + + $this->assertEquals('DIGITAL_GOODS', $testee->category()); + } + + public function testToArray() + { + $unitAmount = Mockery::mock(Money::class); + $unitAmount + ->expects('toArray') + ->andReturn([1]); + $tax = Mockery::mock(Money::class); + $tax + ->expects('toArray') + ->andReturn([2]); + $testee = new Item( + 'name', + $unitAmount, + 1, + 'description', + $tax, + 'sku', + 'PHYSICAL_GOODS' + ); + + $expected = [ + 'name' => 'name', + 'unit_amount' => [1], + 'quantity' => 1, + 'description' => 'description', + 'sku' => 'sku', + 'category' => 'PHYSICAL_GOODS', + 'tax' => [2], + ]; + + $this->assertEquals($expected, $testee->toArray()); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/MoneyTest.php b/tests/PHPUnit/ApiClient/Entity/MoneyTest.php new file mode 100644 index 000000000..986adb268 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/MoneyTest.php @@ -0,0 +1,23 @@ +assertEquals(1.10, $testee->value()); + $this->assertEquals('currencyCode', $testee->currencyCode()); + + $expected = [ + 'currency_code' => 'currencyCode', + 'value' => 1.10, + ]; + $this->assertEquals($expected, $testee->toArray()); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/PayerTest.php b/tests/PHPUnit/ApiClient/Entity/PayerTest.php new file mode 100644 index 000000000..46dde9a71 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/PayerTest.php @@ -0,0 +1,202 @@ +expects('toArray') + ->andReturn(['address']); + $phone = Mockery::mock(PhoneWithType::class); + $phone + ->expects('toArray') + ->andReturn(['phone']); + $taxInfo = Mockery::mock(PayerTaxInfo::class); + $taxInfo + ->expects('toArray') + ->andReturn(['taxInfo']); + $payerName = Mockery::mock(PayerName::class); + $payerName + ->expects('toArray') + ->andReturn(['payerName']); + $email = 'email@example.com'; + $payerId = 'payerId'; + $payer = new Payer( + $payerName, + $email, + $payerId, + $address, + $birthday, + $phone, + $taxInfo + ); + + $this->assertEquals($payerName, $payer->name()); + $this->assertEquals($email, $payer->emailAddress()); + $this->assertEquals($payerId, $payer->payerId()); + $this->assertEquals($address, $payer->address()); + $this->assertEquals($birthday, $payer->birthDate()); + $this->assertEquals($phone, $payer->phone()); + $this->assertEquals($taxInfo, $payer->taxInfo()); + + $array = $payer->toArray(); + $this->assertEquals($birthday->format('Y-m-d'), $array['birth_date']); + $this->assertEquals(['payerName'], $array['name']); + $this->assertEquals($email, $array['email_address']); + $this->assertEquals(['address'], $array['address']); + $this->assertEquals($payerId, $array['payer_id']); + $this->assertEquals(['phone'], $array['phone']); + $this->assertEquals(['taxInfo'], $array['tax_info']); + } + + public function testPayerNoId() + { + $birthday = new \DateTime(); + $address = Mockery::mock(Address::class); + $address + ->expects('toArray') + ->andReturn(['address']); + $phone = Mockery::mock(PhoneWithType::class); + $phone + ->expects('toArray') + ->andReturn(['phone']); + $taxInfo = Mockery::mock(PayerTaxInfo::class); + $taxInfo + ->expects('toArray') + ->andReturn(['taxInfo']); + $payerName = Mockery::mock(PayerName::class); + $payerName + ->expects('toArray') + ->andReturn(['payerName']); + $email = 'email@example.com'; + $payerId = ''; + $payer = new Payer( + $payerName, + $email, + $payerId, + $address, + $birthday, + $phone, + $taxInfo + ); + + $this->assertEquals($payerId, $payer->payerId()); + + $array = $payer->toArray(); + $this->assertArrayNotHasKey('payer_id', $array); + } + + public function testPayerNoPhone() + { + $birthday = new \DateTime(); + $address = Mockery::mock(Address::class); + $address + ->expects('toArray') + ->andReturn(['address']); + $phone = null; + $taxInfo = Mockery::mock(PayerTaxInfo::class); + $taxInfo + ->expects('toArray') + ->andReturn(['taxInfo']); + $payerName = Mockery::mock(PayerName::class); + $payerName + ->expects('toArray') + ->andReturn(['payerName']); + $email = 'email@example.com'; + $payerId = 'payerId'; + $payer = new Payer( + $payerName, + $email, + $payerId, + $address, + $birthday, + $phone, + $taxInfo + ); + + $this->assertEquals($phone, $payer->phone()); + + $array = $payer->toArray(); + $this->assertArrayNotHasKey('phone', $array); + } + + public function testPayerNoTaxInfo() + { + $birthday = new \DateTime(); + $address = Mockery::mock(Address::class); + $address + ->expects('toArray') + ->andReturn(['address']); + $phone = Mockery::mock(PhoneWithType::class); + $phone + ->expects('toArray') + ->andReturn(['phone']); + $taxInfo = null; + $payerName = Mockery::mock(PayerName::class); + $payerName + ->expects('toArray') + ->andReturn(['payerName']); + $email = 'email@example.com'; + $payerId = 'payerId'; + $payer = new Payer( + $payerName, + $email, + $payerId, + $address, + $birthday, + $phone, + $taxInfo + ); + + $this->assertEquals($taxInfo, $payer->taxInfo()); + + $array = $payer->toArray(); + $this->assertArrayNotHasKey('tax_info', $array); + } + + public function testPayerNoBirthDate() + { + $birthday = null; + $address = Mockery::mock(Address::class); + $address + ->expects('toArray') + ->andReturn(['address']); + $phone = Mockery::mock(PhoneWithType::class); + $phone + ->expects('toArray') + ->andReturn(['phone']); + $taxInfo = Mockery::mock(PayerTaxInfo::class); + $taxInfo + ->expects('toArray') + ->andReturn(['taxInfo']); + $payerName = Mockery::mock(PayerName::class); + $payerName + ->expects('toArray') + ->andReturn(['payerName']); + $email = 'email@example.com'; + $payerId = 'payerId'; + $payer = new Payer( + $payerName, + $email, + $payerId, + $address, + $birthday, + $phone, + $taxInfo + ); + + $this->assertEquals($birthday, $payer->birthDate()); + + $array = $payer->toArray(); + $this->assertArrayNotHasKey('birth_date', $array); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php b/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php new file mode 100644 index 000000000..b5c4319ba --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/PaymentsTest.php @@ -0,0 +1,46 @@ +assertEquals($authorizations, $testee->authorizations()); + } + + public function testToArray() + { + $authorization = \Mockery::mock(Authorization::class); + $authorization->shouldReceive('toArray')->andReturn( + [ + 'id' => 'foo', + 'status' => 'CREATED', + ] + ); + $authorizations = [$authorization]; + + $testee = new Payments(...$authorizations); + + $this->assertEquals( + [ + 'authorizations' => [ + [ + 'id' => 'foo', + 'status' => 'CREATED', + ], + ], + ], + $testee->toArray() + ); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php new file mode 100644 index 000000000..2fb5e64b3 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/PurchaseUnitTest.php @@ -0,0 +1,502 @@ + null, + 'toArray' => ['amount'], + ] + ); + + $item1 = Mockery::mock( + Item::class, + [ + 'toArray' => ['item1'], + 'category' => Item::DIGITAL_GOODS, + ] + ); + + $item2 = Mockery::mock( + Item::class, + [ + 'toArray' => ['item2'], + 'category' => Item::PHYSICAL_GOODS, + ] + ); + + $shipping = Mockery::mock(Shipping::class, ['toArray' => ['shipping']]); + + $testee = new PurchaseUnit( + $amount, + [$item1, $item2], + $shipping, + 'referenceId', + 'description', + null, + 'customId', + 'invoiceId', + 'softDescriptor' + ); + + $this->assertEquals($amount, $testee->amount()); + $this->assertEquals('referenceId', $testee->referenceId()); + $this->assertEquals('description', $testee->description()); + $this->assertNull($testee->payee()); + $this->assertEquals('customId', $testee->customId()); + $this->assertEquals('invoiceId', $testee->invoiceId()); + $this->assertEquals('softDescriptor', $testee->softDescriptor()); + $this->assertEquals($shipping, $testee->shipping()); + $this->assertEquals([$item1, $item2], $testee->items()); + self::assertTrue($testee->containsPhysicalGoodsItems()); + + $expected = [ + 'reference_id' => 'referenceId', + 'amount' => ['amount'], + 'description' => 'description', + 'items' => [['item1'], ['item2']], + 'shipping' => ['shipping'], + 'custom_id' => 'customId', + 'invoice_id' => 'invoiceId', + 'soft_descriptor' => 'softDescriptor', + ]; + + $this->assertEquals($expected, $testee->toArray()); + } + + /** + * @dataProvider dataForDitchTests + * @param array $items + * @param Amount $amount + * @param bool $doDitch + */ + public function testDitchMethod(array $items, Amount $amount, bool $doDitch, string $message) + { + $testee = new PurchaseUnit( + $amount, + $items + ); + + $array = $testee->toArray(); + $resultItems = $doDitch === ! array_key_exists('items', $array); + $resultBreakdown = $doDitch === ! array_key_exists('breakdown', $array['amount']); + $this->assertTrue($resultItems, $message); + $this->assertTrue($resultBreakdown, $message); + } + + public function dataForDitchTests() : array + { + $data = [ + 'default' => [ + 'message' => 'Items should not be ditched.', + 'ditch' => false, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'dont_ditch_with_discount' => [ + 'message' => 'Items should not be ditched.', + 'ditch' => false, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 23, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => 3, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'ditch_with_discount' => [ + 'message' => 'Items should be ditched because of discount.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 25, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => 3, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'dont_ditch_with_shipping_discount' => [ + 'message' => 'Items should not be ditched.', + 'ditch' => false, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 23, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => 3, + 'handling' => null, + 'insurance' => null, + ], + ], + 'ditch_with_handling' => [ + 'message' => 'Items should be ditched because of handling.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => 3, + 'insurance' => null, + ], + ], + 'dont_ditch_with_handling' => [ + 'message' => 'Items should not be ditched.', + 'ditch' => false, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 29, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => 3, + 'insurance' => null, + ], + ], + 'ditch_with_insurance' => [ + 'message' => 'Items should be ditched because of insurance.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => 3, + ], + ], + 'dont_ditch_with_insurance' => [ + 'message' => 'Items should not be ditched.', + 'ditch' => false, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 29, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => 3, + ], + ], + 'ditch_with_shipping_discount' => [ + 'message' => 'Items should be ditched because of shipping discount.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 25, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => 3, + 'handling' => null, + 'insurance' => null, + ], + ], + 'dont_ditch_with_shipping' => [ + 'message' => 'Items should not be ditched.', + 'ditch' => false, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 29, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => 3, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'ditch_because_shipping' => [ + 'message' => 'Items should be ditched because of shipping.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 28, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => 3, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'ditch_items_total' => [ + 'message' => 'Items should be ditched because the item total does not add up.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26, + 'breakdown' => [ + 'itemTotal' => 11, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'ditch_tax_total' => [ + 'message' => 'Items should be ditched because the tax total does not add up.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 26, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 5, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + 'ditch_total_amount' => [ + 'message' => 'Items should be ditched because the total amount is way out of order.', + 'ditch' => true, + 'items' => [ + [ + 'value' => 10, + 'quantity' => 2, + 'tax' => 3, + 'category' => Item::PHYSICAL_GOODS, + ], + ], + 'amount' => 260, + 'breakdown' => [ + 'itemTotal' => 20, + 'taxTotal' => 6, + 'shipping' => null, + 'discount' => null, + 'shippingDiscount' => null, + 'handling' => null, + 'insurance' => null, + ], + ], + ]; + + $values = []; + foreach ($data as $testKey => $test) { + $items = []; + foreach ($test['items'] as $key => $item) { + $unitAmount = Mockery::mock(Money::class); + $unitAmount->shouldReceive('value')->andReturn($item['value']); + $tax = Mockery::mock(Money::class); + $tax->shouldReceive('value')->andReturn($item['tax']); + $items[$key] = Mockery::mock( + Item::class, + [ + 'unitAmount' => $unitAmount, + 'tax' => $tax, + 'quantity'=> $item['quantity'], + 'category' => $item['category'], + 'toArray' => [], + ] + ); + } + $breakdown = null; + if ($test['breakdown']) { + $breakdown = Mockery::mock(AmountBreakdown::class); + foreach ($test['breakdown'] as $method => $value) { + $breakdown->shouldReceive($method)->andReturnUsing(function () use ($value) { + if (! is_numeric($value)) { + return null; + } + + $money = Mockery::mock(Money::class); + $money->shouldReceive('value')->andReturn($value); + return $money; + }); + } + } + $amount = Mockery::mock(Amount::class); + $amount->shouldReceive('toArray')->andReturn(['value' => [], 'breakdown' => []]); + $amount->shouldReceive('value')->andReturn($test['amount']); + $amount->shouldReceive('breakdown')->andReturn($breakdown); + + $values[$testKey] = [ + $items, + $amount, + $test['ditch'], + $test['message'], + ]; + } + + return $values; + } + + public function testPayee() + { + $amount = Mockery::mock(Amount::class); + $amount->shouldReceive('breakdown')->andReturnNull(); + $amount->shouldReceive('toArray')->andReturn(['amount']); + $item1 = Mockery::mock(Item::class); + $item1->shouldReceive('toArray')->andReturn(['item1']); + $item2 = Mockery::mock(Item::class); + $item2->shouldReceive('toArray')->andReturn(['item2']); + $shipping = Mockery::mock(Shipping::class); + $shipping->shouldReceive('toArray')->andReturn(['shipping']); + $payee = Mockery::mock(Payee::class); + $payee->shouldReceive('toArray')->andReturn(['payee']); + $testee = new PurchaseUnit( + $amount, + [], + $shipping, + 'referenceId', + 'description', + $payee, + 'customId', + 'invoiceId', + 'softDescriptor' + ); + + $this->assertEquals($payee, $testee->payee()); + + $expected = [ + 'reference_id' => 'referenceId', + 'amount' => ['amount'], + 'description' => 'description', + 'items' => [], + 'shipping' => ['shipping'], + 'custom_id' => 'customId', + 'invoice_id' => 'invoiceId', + 'soft_descriptor' => 'softDescriptor', + 'payee' => ['payee'], + ]; + + $this->assertEquals($expected, $testee->toArray()); + } +} diff --git a/tests/PHPUnit/ApiClient/Entity/TokenTest.php b/tests/PHPUnit/ApiClient/Entity/TokenTest.php new file mode 100644 index 000000000..23d4c7dc5 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Entity/TokenTest.php @@ -0,0 +1,137 @@ +assertEquals($data->token, $token->token()); + $this->assertTrue($token->isValid()); + } + + public function dataForTestDefault() : array + { + return [ + 'default' => [ + (object)[ + 'created' => time(), + 'expires_in' => 100, + 'token' => 'abc', + ], + ], + 'created_not_needed' => [ + (object)[ + 'expires_in' => 100, + 'token' => 'abc', + ], + ], + ]; + } + + public function testIsValid() + { + $data = (object) [ + 'created' => time() - 100, + 'expires_in' => 99, + 'token' => 'abc', + ]; + + $token = new Token($data); + $this->assertFalse($token->isValid()); + } + + public function testFromBearerJson() + { + $data = json_encode([ + 'expires_in' => 100, + 'access_token' => 'abc', + ]); + + $token = Token::fromJson($data); + $this->assertEquals('abc', $token->token()); + $this->assertTrue($token->isValid()); + } + + public function testFromIdentityJson() + { + $data = json_encode([ + 'expires_in' => 100, + 'client_token' => 'abc', + ]); + + $token = Token::fromJson($data); + $this->assertEquals('abc', $token->token()); + $this->assertTrue($token->isValid()); + } + + public function testAsJson() + { + $data = (object) [ + 'created' => 100, + 'expires_in' => 100, + 'token' => 'abc', + ]; + + $token = new Token($data); + $json = json_decode($token->asJson()); + $this->assertEquals($data->token, $json->token); + $this->assertEquals($data->created, $json->created); + $this->assertEquals($data->expires_in, $json->expires_in); + } + + /** + * @dataProvider dataForTestExceptions + * @param \stdClass $data + */ + public function testExceptions(\stdClass $data) + { + $this->expectException(RuntimeException::class); + new Token($data); + } + + public function dataForTestExceptions() : array + { + return [ + 'created_is_not_integer' => [ + (object) [ + 'created' => 'abc', + 'expires_in' => 123, + 'token' => 'abc', + ], + ], + 'expires_in_is_not_integer' => [ + (object) [ + 'expires_in' => 'abc', + 'token' => 'abc', + ], + ], + 'access_token_is_not_string' => [ + (object) [ + 'expires_in' => 123, + 'token' => ['abc'], + ], + ], + 'access_token_does_not_exist' => [ + (object) [ + 'expires_in' => 123, + ], + ], + 'expires_in_does_not_exist' => [ + (object) [ + 'token' => 'abc', + ], + ], + ]; + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/AddressFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/AddressFactoryTest.php new file mode 100644 index 000000000..49cb00fe6 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/AddressFactoryTest.php @@ -0,0 +1,205 @@ +expects('get_shipping_country') + ->andReturn('shipping_country'); + $customer + ->expects('get_shipping_address_1') + ->andReturn('shipping_address_1'); + $customer + ->expects('get_shipping_address_2') + ->andReturn('shipping_address_2'); + $customer + ->expects('get_shipping_state') + ->andReturn('shipping_state'); + $customer + ->expects('get_shipping_city') + ->andReturn('shipping_city'); + $customer + ->expects('get_shipping_postcode') + ->andReturn('shipping_postcode'); + + $result = $testee->fromWcCustomer($customer); + $this->assertEquals('shipping_country', $result->countryCode()); + $this->assertEquals('shipping_address_1', $result->addressLine1()); + $this->assertEquals('shipping_address_2', $result->addressLine2()); + $this->assertEquals('shipping_state', $result->adminArea1()); + $this->assertEquals('shipping_city', $result->adminArea2()); + $this->assertEquals('shipping_postcode', $result->postalCode()); + } + + public function testFromWcCustomersBillingAddress() + { + $testee = new AddressFactory(); + $customer = Mockery::mock(\WC_Customer::class); + $customer + ->expects('get_billing_country') + ->andReturn('billing_country'); + $customer + ->expects('get_billing_address_1') + ->andReturn('billing_address_1'); + $customer + ->expects('get_billing_address_2') + ->andReturn('billing_address_2'); + $customer + ->expects('get_billing_state') + ->andReturn('billing_state'); + $customer + ->expects('get_billing_city') + ->andReturn('billing_city'); + $customer + ->expects('get_billing_postcode') + ->andReturn('billing_postcode'); + + $result = $testee->fromWcCustomer($customer, 'billing'); + $this->assertEquals('billing_country', $result->countryCode()); + $this->assertEquals('billing_address_1', $result->addressLine1()); + $this->assertEquals('billing_address_2', $result->addressLine2()); + $this->assertEquals('billing_state', $result->adminArea1()); + $this->assertEquals('billing_city', $result->adminArea2()); + $this->assertEquals('billing_postcode', $result->postalCode()); + } + + public function testFromWcOrder() + { + $testee = new AddressFactory(); + $order = Mockery::mock(\WC_Order::class); + $order + ->expects('get_shipping_country') + ->andReturn('shipping_country'); + $order + ->expects('get_shipping_address_1') + ->andReturn('shipping_address_1'); + $order + ->expects('get_shipping_address_2') + ->andReturn('shipping_address_2'); + $order + ->expects('get_shipping_state') + ->andReturn('shipping_state'); + $order + ->expects('get_shipping_city') + ->andReturn('shipping_city'); + $order + ->expects('get_shipping_postcode') + ->andReturn('shipping_postcode'); + + $result = $testee->fromWcOrder($order); + $this->assertEquals('shipping_country', $result->countryCode()); + $this->assertEquals('shipping_address_1', $result->addressLine1()); + $this->assertEquals('shipping_address_2', $result->addressLine2()); + $this->assertEquals('shipping_state', $result->adminArea1()); + $this->assertEquals('shipping_city', $result->adminArea2()); + $this->assertEquals('shipping_postcode', $result->postalCode()); + } + + /** + * @dataProvider dataFromPayPalRequest + */ + public function testFromPayPalRequest($data) + { + $testee = new AddressFactory(); + + $result = $testee->fromPayPalRequest($data); + $expectedAddressLine1 = (isset($data->address_line_1)) ? $data->address_line_1 : ''; + $expectedAddressLine2 = (isset($data->address_line_2)) ? $data->address_line_2 : ''; + $expectedAdminArea1 = (isset($data->admin_area_1)) ? $data->admin_area_1 : ''; + $expectedAdminArea2 = (isset($data->admin_area_2)) ? $data->admin_area_2 : ''; + $expectedPostalCode = (isset($data->postal_code)) ? $data->postal_code : ''; + $this->assertEquals($data->country_code, $result->countryCode()); + $this->assertEquals($expectedAddressLine1, $result->addressLine1()); + $this->assertEquals($expectedAddressLine2, $result->addressLine2()); + $this->assertEquals($expectedAdminArea1, $result->adminArea1()); + $this->assertEquals($expectedAdminArea2, $result->adminArea2()); + $this->assertEquals($expectedPostalCode, $result->postalCode()); + } + + public function testFromPayPalRequestThrowsError() + { + $testee = new AddressFactory(); + + $data = (object) [ + 'address_line_1' => 'shipping_address_1', + 'address_line_2' => 'shipping_address_2', + 'admin_area_1' => 'shipping_admin_area_1', + 'admin_area_2' => 'shipping_admin_area_2', + 'postal_code' => 'shipping_postcode', + ]; + $this->expectException(RuntimeException::class); + $testee->fromPayPalRequest($data); + } + + public function dataFromPayPalRequest() : array + { + return [ + 'default' => [ + (object) [ + 'country_code' => 'shipping_country', + 'address_line_1' => 'shipping_address_1', + 'address_line_2' => 'shipping_address_2', + 'admin_area_1' => 'shipping_admin_area_1', + 'admin_area_2' => 'shipping_admin_area_2', + 'postal_code' => 'shipping_postcode', + ], + ], + 'no_admin_area_2' => [ + (object) [ + 'country_code' => 'shipping_country', + 'address_line_1' => 'shipping_address_1', + 'address_line_2' => 'shipping_address_2', + 'admin_area_1' => 'shipping_admin_area_1', + 'postal_code' => 'shipping_postcode', + ], + ], + 'no_postal_code' => [ + (object) [ + 'country_code' => 'shipping_country', + 'address_line_1' => 'shipping_address_1', + 'address_line_2' => 'shipping_address_2', + 'admin_area_1' => 'shipping_admin_area_1', + 'admin_area_2' => 'shipping_admin_area_2', + ], + ], + 'no_admin_area_1' => [ + (object) [ + 'country_code' => 'shipping_country', + 'address_line_1' => 'shipping_address_1', + 'address_line_2' => 'shipping_address_2', + 'admin_area_2' => 'shipping_admin_area_2', + 'postal_code' => 'shipping_postcode', + ], + ], + 'no_address_line_1' => [ + (object) [ + 'country_code' => 'shipping_country', + 'address_line_2' => 'shipping_address_2', + 'admin_area_1' => 'shipping_admin_area_1', + 'admin_area_2' => 'shipping_admin_area_2', + 'postal_code' => 'shipping_postcode', + ], + ], + 'no_address_line_2' => [ + (object) [ + 'country_code' => 'shipping_country', + 'address_line_1' => 'shipping_address_1', + 'admin_area_1' => 'shipping_admin_area_1', + 'admin_area_2' => 'shipping_admin_area_2', + 'postal_code' => 'shipping_postcode', + ], + ], + ]; + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/AmountFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/AmountFactoryTest.php new file mode 100644 index 000000000..bc98bed1e --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/AmountFactoryTest.php @@ -0,0 +1,581 @@ +shouldReceive('get_total') + ->withAnyArgs() + ->andReturn(1); + $cart + ->shouldReceive('get_cart_contents_total') + ->andReturn(2); + $cart + ->shouldReceive('get_discount_total') + ->andReturn(3); + $cart + ->shouldReceive('get_shipping_total') + ->andReturn(4); + $cart + ->shouldReceive('get_shipping_tax') + ->andReturn(5); + $cart + ->shouldReceive('get_cart_contents_tax') + ->andReturn(6); + $cart + ->shouldReceive('get_discount_tax') + ->andReturn(7); + + expect('get_woocommerce_currency')->andReturn($expectedCurrency); + $result = $testee->fromWcCart($cart); + $this->assertEquals($expectedCurrency, $result->currencyCode()); + $this->assertEquals((float) 1, $result->value()); + $this->assertEquals((float) 10, $result->breakdown()->discount()->value()); + $this->assertEquals($expectedCurrency, $result->breakdown()->discount()->currencyCode()); + $this->assertEquals((float) 9, $result->breakdown()->shipping()->value()); + $this->assertEquals($expectedCurrency, $result->breakdown()->shipping()->currencyCode()); + $this->assertEquals((float) 5, $result->breakdown()->itemTotal()->value()); + $this->assertEquals($expectedCurrency, $result->breakdown()->itemTotal()->currencyCode()); + $this->assertEquals((float) 13, $result->breakdown()->taxTotal()->value()); + $this->assertEquals($expectedCurrency, $result->breakdown()->taxTotal()->currencyCode()); + } + + public function testFromWcCartNoDiscount() + { + $itemFactory = Mockery::mock(ItemFactory::class); + $testee = new AmountFactory($itemFactory); + + $expectedCurrency = 'EUR'; + $expectedTotal = 1; + $cart = Mockery::mock(\WC_Cart::class); + $cart + ->shouldReceive('get_total') + ->withAnyArgs() + ->andReturn($expectedTotal); + $cart + ->shouldReceive('get_cart_contents_total') + ->andReturn(2); + $cart + ->shouldReceive('get_discount_total') + ->andReturn(0); + $cart + ->shouldReceive('get_shipping_total') + ->andReturn(4); + $cart + ->shouldReceive('get_shipping_tax') + ->andReturn(5); + $cart + ->shouldReceive('get_cart_contents_tax') + ->andReturn(6); + $cart + ->shouldReceive('get_discount_tax') + ->andReturn(0); + + expect('get_woocommerce_currency')->andReturn($expectedCurrency); + $result = $testee->fromWcCart($cart); + $this->assertNull($result->breakdown()->discount()); + } + + public function testFromWcOrderDefault() + { + $itemFactory = Mockery::mock(ItemFactory::class); + $order = Mockery::mock(\WC_Order::class); + $unitAmount = Mockery::mock(Money::class); + $unitAmount + ->shouldReceive('value') + ->andReturn(3); + $tax = Mockery::mock(Money::class); + $tax + ->shouldReceive('value') + ->andReturn(1); + $item = Mockery::mock(Item::class); + $item + ->shouldReceive('quantity') + ->andReturn(2); + $item + ->shouldReceive('unitAmount') + ->andReturn($unitAmount); + $item + ->shouldReceive('tax') + ->andReturn($tax); + $itemFactory + ->expects('fromWcOrder') + ->with($order) + ->andReturn([$item]); + $testee = new AmountFactory($itemFactory); + + $expectedCurrency = 'EUR'; + $order + ->shouldReceive('get_total') + ->andReturn(100); + $order + ->shouldReceive('get_currency') + ->andReturn($expectedCurrency); + $order + ->shouldReceive('get_shipping_total') + ->andReturn(1); + $order + ->shouldReceive('get_shipping_tax') + ->andReturn(.5); + $order + ->shouldReceive('get_total_discount') + ->with(false) + ->andReturn(3); + + $result = $testee->fromWcOrder($order); + $this->assertEquals((float) 3, $result->breakdown()->discount()->value()); + $this->assertEquals((float) 6, $result->breakdown()->itemTotal()->value()); + $this->assertEquals((float) 1.5, $result->breakdown()->shipping()->value()); + $this->assertEquals((float) 100, $result->value()); + $this->assertEquals((float) 2, $result->breakdown()->taxTotal()->value()); + $this->assertEquals($expectedCurrency, $result->breakdown()->discount()->currencyCode()); + $this->assertEquals($expectedCurrency, $result->breakdown()->itemTotal()->currencyCode()); + $this->assertEquals($expectedCurrency, $result->breakdown()->shipping()->currencyCode()); + $this->assertEquals($expectedCurrency, $result->breakdown()->taxTotal()->currencyCode()); + $this->assertEquals($expectedCurrency, $result->currencyCode()); + } + + public function testFromWcOrderDiscountIsNull() + { + $itemFactory = Mockery::mock(ItemFactory::class); + $order = Mockery::mock(\WC_Order::class); + $unitAmount = Mockery::mock(Money::class); + $unitAmount + ->shouldReceive('value') + ->andReturn(3); + $tax = Mockery::mock(Money::class); + $tax + ->shouldReceive('value') + ->andReturn(1); + $item = Mockery::mock(Item::class); + $item + ->shouldReceive('quantity') + ->andReturn(2); + $item + ->shouldReceive('unitAmount') + ->andReturn($unitAmount); + $item + ->shouldReceive('tax') + ->andReturn($tax); + $itemFactory + ->expects('fromWcOrder') + ->with($order) + ->andReturn([$item]); + $testee = new AmountFactory($itemFactory); + + $expectedCurrency = 'EUR'; + $order + ->shouldReceive('get_total') + ->andReturn(100); + $order + ->shouldReceive('get_currency') + ->andReturn($expectedCurrency); + $order + ->shouldReceive('get_shipping_total') + ->andReturn(1); + $order + ->shouldReceive('get_shipping_tax') + ->andReturn(.5); + $order + ->shouldReceive('get_total_discount') + ->with(false) + ->andReturn(0); + + $result = $testee->fromWcOrder($order); + $this->assertNull($result->breakdown()->discount()); + } + + /** + * @dataProvider dataFromPayPalResponse + * @param $response + */ + public function testFromPayPalResponse($response, $expectsException) + { + $itemFactory = Mockery::mock(ItemFactory::class); + $testee = new AmountFactory($itemFactory); + if ($expectsException) { + $this->expectException(RuntimeException::class); + } + $result = $testee->fromPayPalResponse($response); + if ($expectsException) { + return; + } + $this->assertEquals($response->value, $result->value()); + $this->assertEquals($response->currency_code, $result->currencyCode()); + $breakdown = $result->breakdown(); + if (! isset($response->breakdown)) { + $this->assertNull($breakdown); + return; + } + if ($breakdown->shipping()) { + $this->assertEquals($response->breakdown->shipping->value, $breakdown->shipping()->value()); + $this->assertEquals($response->breakdown->shipping->currency_code, $breakdown->shipping()->currencyCode()); + } else { + $this->assertTrue(! isset($response->breakdown->shipping)); + } + if ($breakdown->itemTotal()) { + $this->assertEquals($response->breakdown->item_total->value, $breakdown->itemTotal()->value()); + $this->assertEquals($response->breakdown->item_total->currency_code, $breakdown->itemTotal()->currencyCode()); + } else { + $this->assertTrue(! isset($response->breakdown->item_total)); + } + if ($breakdown->taxTotal()) { + $this->assertEquals($response->breakdown->tax_total->value, $breakdown->taxTotal()->value()); + $this->assertEquals($response->breakdown->tax_total->currency_code, $breakdown->taxTotal()->currencyCode()); + } else { + $this->assertTrue(! isset($response->breakdown->tax_total)); + } + if ($breakdown->handling()) { + $this->assertEquals($response->breakdown->handling->value, $breakdown->handling()->value()); + $this->assertEquals($response->breakdown->handling->currency_code, $breakdown->handling()->currencyCode()); + } else { + $this->assertTrue(! isset($response->breakdown->handling)); + } + if ($breakdown->insurance()) { + $this->assertEquals($response->breakdown->insurance->value, $breakdown->insurance()->value()); + $this->assertEquals($response->breakdown->insurance->currency_code, $breakdown->insurance()->currencyCode()); + } else { + $this->assertTrue(! isset($response->breakdown->insurance)); + } + if ($breakdown->shippingDiscount()) { + $this->assertEquals($response->breakdown->shipping_discount->value, $breakdown->shippingDiscount()->value()); + $this->assertEquals($response->breakdown->shipping_discount->currency_code, $breakdown->shippingDiscount()->currencyCode()); + } else { + $this->assertTrue(! isset($response->breakdown->shipping_discount)); + } + if ($breakdown->discount()) { + $this->assertEquals($response->breakdown->discount->value, $breakdown->discount()->value()); + $this->assertEquals($response->breakdown->discount->currency_code, $breakdown->discount()->currencyCode()); + } else { + $this->assertTrue(! isset($response->breakdown->discount)); + } + } + + public function dataFromPayPalResponse() : array + { + return [ + 'no_value' => [ + (object) [ + "currency_code" => "A", + ], + true, + ], + 'no_currency_code' => [ + (object) [ + "value" => (float) 1, + ], + true, + ], + 'no_value_in_breakdown' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "currency_code" => "B", + ], + ], + ], + true, + ], + 'no_currency_code_in_breakdown' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + ], + ], + ], + true, + ], + 'default' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + "currency_code" => "B", + ], + "shipping_discount" => (object) [ + "value" => (float) 3, + "currency_code" => "C", + ], + "insurance" => (object) [ + "value" => (float) 4, + "currency_code" => "D", + ], + "handling" => (object) [ + "value" => (float) 5, + "currency_code" => "E", + ], + "tax_total" => (object) [ + "value" => (float) 6, + "currency_code" => "F", + ], + "shipping" => (object) [ + "value" => (float) 7, + "currency_code" => "G", + ], + "item_total" => (object) [ + "value" => (float) 8, + "currency_code" => "H", + ], + ], + ], + false, + ], + 'no_item_total' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + "currency_code" => "B", + ], + "shipping_discount" => (object) [ + "value" => (float) 3, + "currency_code" => "C", + ], + "insurance" => (object) [ + "value" => (float) 4, + "currency_code" => "D", + ], + "handling" => (object) [ + "value" => (float) 5, + "currency_code" => "E", + ], + "tax_total" => (object) [ + "value" => (float) 6, + "currency_code" => "F", + ], + "shipping" => (object) [ + "value" => (float) 7, + "currency_code" => "G", + ], + ], + ], + false, + ], + 'no_tax_total' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + "currency_code" => "B", + ], + "shipping_discount" => (object) [ + "value" => (float) 3, + "currency_code" => "C", + ], + "insurance" => (object) [ + "value" => (float) 4, + "currency_code" => "D", + ], + "handling" => (object) [ + "value" => (float) 5, + "currency_code" => "E", + ], + "shipping" => (object) [ + "value" => (float) 7, + "currency_code" => "G", + ], + "item_total" => (object) [ + "value" => (float) 8, + "currency_code" => "H", + ], + ], + ], + false, + ], + 'no_handling' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + "currency_code" => "B", + ], + "shipping_discount" => (object) [ + "value" => (float) 3, + "currency_code" => "C", + ], + "insurance" => (object) [ + "value" => (float) 4, + "currency_code" => "D", + ], + "tax_total" => (object) [ + "value" => (float) 6, + "currency_code" => "F", + ], + "shipping" => (object) [ + "value" => (float) 7, + "currency_code" => "G", + ], + "item_total" => (object) [ + "value" => (float) 8, + "currency_code" => "H", + ], + ], + ], + false, + ], + 'no_insurance' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + "currency_code" => "B", + ], + "shipping_discount" => (object) [ + "value" => (float) 3, + "currency_code" => "C", + ], + "handling" => (object) [ + "value" => (float) 5, + "currency_code" => "E", + ], + "tax_total" => (object) [ + "value" => (float) 6, + "currency_code" => "F", + ], + "shipping" => (object) [ + "value" => (float) 7, + "currency_code" => "G", + ], + "item_total" => (object) [ + "value" => (float) 8, + "currency_code" => "H", + ], + ], + ], + false, + ], + 'no_shipping_discount' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + "currency_code" => "B", + ], + "insurance" => (object) [ + "value" => (float) 4, + "currency_code" => "D", + ], + "handling" => (object) [ + "value" => (float) 5, + "currency_code" => "E", + ], + "tax_total" => (object) [ + "value" => (float) 6, + "currency_code" => "F", + ], + "shipping" => (object) [ + "value" => (float) 7, + "currency_code" => "G", + ], + "item_total" => (object) [ + "value" => (float) 8, + "currency_code" => "H", + ], + ], + ], + false, + ], + 'no_discount' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "shipping_discount" => (object) [ + "value" => (float) 3, + "currency_code" => "C", + ], + "insurance" => (object) [ + "value" => (float) 4, + "currency_code" => "D", + ], + "handling" => (object) [ + "value" => (float) 5, + "currency_code" => "E", + ], + "tax_total" => (object) [ + "value" => (float) 6, + "currency_code" => "F", + ], + "shipping" => (object) [ + "value" => (float) 7, + "currency_code" => "G", + ], + "item_total" => (object) [ + "value" => (float) 8, + "currency_code" => "H", + ], + ], + ], + false, + ], + 'no_shipping' => [ + (object) [ + "value" => (float) 1, + "currency_code" => "A", + "breakdown" => (object) [ + "discount" => (object) [ + "value" => (float) 2, + "currency_code" => "B", + ], + "shipping_discount" => (object) [ + "value" => (float) 3, + "currency_code" => "C", + ], + "insurance" => (object) [ + "value" => (float) 4, + "currency_code" => "D", + ], + "handling" => (object) [ + "value" => (float) 5, + "currency_code" => "E", + ], + "tax_total" => (object) [ + "value" => (float) 6, + "currency_code" => "F", + ], + "item_total" => (object) [ + "value" => (float) 8, + "currency_code" => "H", + ], + ], + ], + false, + ], + ]; + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php new file mode 100644 index 000000000..4544f35ce --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/AuthorizationFactoryTest.php @@ -0,0 +1,53 @@ + 'foo', + 'status' => 'CAPTURED', + ]; + + $testee = new AuthorizationFactory(); + $result = $testee->fromPayPalRequest($response); + + $this->assertInstanceOf(Authorization::class, $result); + + $this->assertEquals('foo', $result->id()); + $this->assertInstanceOf(AuthorizationStatus::class, $result->status()); + + $this->assertEquals('CAPTURED', $result->status()->name()); + } + + public function testReturnExceptionIdIsMissing() + { + $this->expectException(RuntimeException::class); + $response = (object)[ + 'status' => 'CAPTURED', + ]; + + $testee = new AuthorizationFactory(); + $testee->fromPayPalRequest($response); + } + + public function testReturnExceptionStatusIsMissing() + { + $this->expectException(RuntimeException::class); + $response = (object)[ + 'id' => 'foo', + ]; + + $testee = new AuthorizationFactory(); + $testee->fromPayPalRequest($response); + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php new file mode 100644 index 000000000..ba96a7d53 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/ItemFactoryTest.php @@ -0,0 +1,420 @@ +expects('get_name') + ->andReturn('name'); + $product + ->expects('get_description') + ->andReturn('description'); + $product + ->expects('get_sku') + ->andReturn('sku'); + $product + ->expects('is_virtual') + ->andReturn(false); + $items = [ + [ + 'data' => $product, + 'quantity' => 1, + ], + ]; + $cart = Mockery::mock(\WC_Cart::class); + $cart + ->expects('get_cart_contents') + ->andReturn($items); + + expect('get_woocommerce_currency') + ->andReturn('EUR'); + expect('wc_get_price_including_tax') + ->with($product) + ->andReturn(2.995); + expect('wc_get_price_excluding_tax') + ->with($product) + ->andReturn(1); + + $result = $testee->fromWcCart($cart); + + $this->assertCount(1, $result); + $item = current($result); + $this->assertInstanceOf(Item::class, $item); + /** + * @var Item $item + */ + $this->assertEquals(Item::PHYSICAL_GOODS, $item->category()); + $this->assertEquals('description', $item->description()); + $this->assertEquals(1, $item->quantity()); + $this->assertEquals('name', $item->name()); + $this->assertEquals('sku', $item->sku()); + $this->assertEquals(1, $item->unitAmount()->value()); + $this->assertEquals(2, $item->tax()->value()); + } + + public function testFromCartDigitalGood() + { + $testee = new ItemFactory(); + + $product = Mockery::mock(\WC_Product_Simple::class); + $product + ->expects('get_name') + ->andReturn('name'); + $product + ->expects('get_description') + ->andReturn('description'); + $product + ->expects('get_sku') + ->andReturn('sku'); + $product + ->expects('is_virtual') + ->andReturn(true); + $items = [ + [ + 'data' => $product, + 'quantity' => 1, + ], + ]; + $cart = Mockery::mock(\WC_Cart::class); + $cart + ->expects('get_cart_contents') + ->andReturn($items); + + expect('get_woocommerce_currency') + ->andReturn('EUR'); + expect('wc_get_price_including_tax') + ->with($product) + ->andReturn(2.995); + expect('wc_get_price_excluding_tax') + ->with($product) + ->andReturn(1); + + $result = $testee->fromWcCart($cart); + + $item = current($result); + $this->assertEquals(Item::DIGITAL_GOODS, $item->category()); + } + + public function testFromWcOrderDefault() + { + $testee = new ItemFactory(); + + $product = Mockery::mock(\WC_Product::class); + $product + ->expects('get_name') + ->andReturn('name'); + $product + ->expects('get_description') + ->andReturn('description'); + $product + ->expects('get_sku') + ->andReturn('sku'); + $product + ->expects('is_virtual') + ->andReturn(false); + + $item = Mockery::mock(\WC_Order_Item_Product::class); + $item + ->expects('get_product') + ->andReturn($product); + $item + ->expects('get_quantity') + ->andReturn(1); + + $order = Mockery::mock(\WC_Order::class); + $order + ->expects('get_currency') + ->andReturn('EUR'); + $order + ->expects('get_items') + ->andReturn([$item]); + $order + ->expects('get_item_subtotal') + ->with($item, true) + ->andReturn(3); + $order + ->expects('get_item_subtotal') + ->with($item, false) + ->andReturn(1); + + $result = $testee->fromWcOrder($order); + $this->assertCount(1, $result); + $item = current($result); + /** + * @var Item $item + */ + $this->assertInstanceOf(Item::class, $item); + $this->assertEquals('name', $item->name()); + $this->assertEquals('description', $item->description()); + $this->assertEquals(1, $item->quantity()); + $this->assertEquals(Item::PHYSICAL_GOODS, $item->category()); + $this->assertEquals(1, $item->unitAmount()->value()); + $this->assertEquals(2, $item->tax()->value()); + } + + public function testFromWcOrderDigitalGood() + { + $testee = new ItemFactory(); + + $product = Mockery::mock(\WC_Product::class); + $product + ->expects('get_name') + ->andReturn('name'); + $product + ->expects('get_description') + ->andReturn('description'); + $product + ->expects('get_sku') + ->andReturn('sku'); + $product + ->expects('is_virtual') + ->andReturn(true); + + $item = Mockery::mock(\WC_Order_Item_Product::class); + $item + ->expects('get_product') + ->andReturn($product); + $item + ->expects('get_quantity') + ->andReturn(1); + + $order = Mockery::mock(\WC_Order::class); + $order + ->expects('get_currency') + ->andReturn('EUR'); + $order + ->expects('get_items') + ->andReturn([$item]); + $order + ->expects('get_item_subtotal') + ->with($item, true) + ->andReturn(3); + $order + ->expects('get_item_subtotal') + ->with($item, false) + ->andReturn(1); + + $result = $testee->fromWcOrder($order); + $item = current($result); + /** + * @var Item $item + */ + $this->assertEquals(Item::DIGITAL_GOODS, $item->category()); + } + + public function testFromWcOrderMaxStringLength() + { + $name = 'öawjetöagrjjaglörjötairgjaflkögjöalfdgjöalfdjblköajtlkfjdbljslkgjfklösdgjalkerjtlrajglkfdajblköajflköbjsdgjadfgjaöfgjaölkgjkladjgfköajgjaflgöjafdlgjafdögjdsflkgjö4jwegjfsdbvxj öskögjtaeröjtrgt'; + $description = 'öawjetöagrjjaglörjötairgjaflkögjöalfdgjöalfdjblköajtlkfjdbljslkgjfklösdgjalkerjtlrajglkfdajblköajflköbjsdgjadfgjaöfgjaölkgjkladjgfköajgjaflgöjafdlgjafdögjdsflkgjö4jwegjfsdbvxj öskögjtaeröjtrgt'; + $testee = new ItemFactory(); + + $product = Mockery::mock(\WC_Product::class); + $product + ->expects('get_name') + ->andReturn($name); + $product + ->expects('get_description') + ->andReturn($description); + $product + ->expects('get_sku') + ->andReturn('sku'); + $product + ->expects('is_virtual') + ->andReturn(true); + + $item = Mockery::mock(\WC_Order_Item_Product::class); + $item + ->expects('get_product') + ->andReturn($product); + $item + ->expects('get_quantity') + ->andReturn(1); + + $order = Mockery::mock(\WC_Order::class); + $order + ->expects('get_currency') + ->andReturn('EUR'); + $order + ->expects('get_items') + ->andReturn([$item]); + $order + ->expects('get_item_subtotal') + ->with($item, true) + ->andReturn(3); + $order + ->expects('get_item_subtotal') + ->with($item, false) + ->andReturn(1); + + $result = $testee->fromWcOrder($order); + $item = current($result); + /** + * @var Item $item + */ + $this->assertEquals(mb_substr($name, 0, 127), $item->name()); + $this->assertEquals(mb_substr($description, 0, 127), $item->description()); + } + + public function testFromPayPalResponse() + { + $testee = new ItemFactory(); + + $response = (object) [ + 'name' => 'name', + 'description' => 'description', + 'quantity' => 1, + 'unit_amount' => (object) [ + 'value' => 1, + 'currency_code' => 'EUR', + ], + ]; + $item = $testee->fromPayPalResponse($response); + $this->assertInstanceOf(Item::class, $item); + /** + * @var Item $item + */ + $this->assertInstanceOf(Item::class, $item); + $this->assertEquals('name', $item->name()); + $this->assertEquals('description', $item->description()); + $this->assertEquals(1, $item->quantity()); + $this->assertEquals(Item::PHYSICAL_GOODS, $item->category()); + $this->assertEquals(1, $item->unitAmount()->value()); + $this->assertNull($item->tax()); + } + + public function testFromPayPalResponseDigitalGood() + { + $testee = new ItemFactory(); + + $response = (object) [ + 'name' => 'name', + 'description' => 'description', + 'quantity' => 1, + 'unit_amount' => (object) [ + 'value' => 1, + 'currency_code' => 'EUR', + ], + 'category' => Item::DIGITAL_GOODS, + ]; + $item = $testee->fromPayPalResponse($response); + /** + * @var Item $item + */ + $this->assertEquals(Item::DIGITAL_GOODS, $item->category()); + } + + public function testFromPayPalResponseHasTax() + { + $testee = new ItemFactory(); + + $response = (object) [ + 'name' => 'name', + 'description' => 'description', + 'quantity' => 1, + 'unit_amount' => (object) [ + 'value' => 1, + 'currency_code' => 'EUR', + ], + 'tax' => (object) [ + 'value' => 100, + 'currency_code' => 'EUR', + ], + ]; + $item = $testee->fromPayPalResponse($response); + $this->assertEquals(100, $item->tax()->value()); + } + + public function testFromPayPalResponseThrowsWithoutName() + { + $testee = new ItemFactory(); + + $response = (object) [ + 'description' => 'description', + 'quantity' => 1, + 'unit_amount' => (object) [ + 'value' => 1, + 'currency_code' => 'EUR', + ], + 'tax' => (object) [ + 'value' => 100, + 'currency_code' => 'EUR', + ], + ]; + $this->expectException(RuntimeException::class); + $testee->fromPayPalResponse($response); + } + + public function testFromPayPalResponseThrowsWithoutQuantity() + { + $testee = new ItemFactory(); + + $response = (object) [ + 'name' => 'name', + 'description' => 'description', + 'unit_amount' => (object) [ + 'value' => 1, + 'currency_code' => 'EUR', + ], + 'tax' => (object) [ + 'value' => 100, + 'currency_code' => 'EUR', + ], + ]; + $this->expectException(RuntimeException::class); + $testee->fromPayPalResponse($response); + } + + public function testFromPayPalResponseThrowsWithStringInQuantity() + { + $testee = new ItemFactory(); + + $response = (object) [ + 'name' => 'name', + 'description' => 'description', + 'quantity' => 'should-not-be-a-string', + 'unit_amount' => (object) [ + 'value' => 1, + 'currency_code' => 'EUR', + ], + 'tax' => (object) [ + 'value' => 100, + 'currency_code' => 'EUR', + ], + ]; + $this->expectException(RuntimeException::class); + $testee->fromPayPalResponse($response); + } + + public function testFromPayPalResponseThrowsWithWrongUnitAmount() + { + $testee = new ItemFactory(); + + $response = (object) [ + 'name' => 'name', + 'description' => 'description', + 'quantity' => 1, + 'unit_amount' => (object) [ + ], + 'tax' => (object) [ + 'value' => 100, + 'currency_code' => 'EUR', + ], + ]; + $this->expectException(RuntimeException::class); + $testee->fromPayPalResponse($response); + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php new file mode 100644 index 000000000..2a4988c9c --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/OrderFactoryTest.php @@ -0,0 +1,221 @@ +expects('id')->andReturn('id'); + $order->expects('status')->andReturn($status); + $order->expects('payer')->andReturn($payer); + $order->expects('intent')->andReturn('intent'); + $order->expects('createTime')->andReturn($createTime); + $order->expects('updateTime')->andReturn($updateTime); + $order->expects('applicationContext')->andReturnNull(); + $order->expects('paymentSource')->andReturnNull(); + $wcOrder = Mockery::mock(\WC_Order::class); + $purchaseUnitFactory = Mockery::mock(PurchaseUnitFactory::class); + $purchaseUnit = Mockery::mock(PurchaseUnit::class); + $purchaseUnitFactory->expects('fromWcOrder')->with($wcOrder)->andReturn($purchaseUnit); + $payerFactory = Mockery::mock(PayerFactory::class); + $applicationRepository = Mockery::mock(ApplicationContextRepository::class); + $applicationFactory = Mockery::mock(ApplicationContextFactory::class); + $paymentSourceFactory = Mockery::mock(PaymentSourceFactory::class); + + + $testee = new OrderFactory( + $purchaseUnitFactory, + $payerFactory, + $applicationRepository, + $applicationFactory, + $paymentSourceFactory + ); + $result = $testee->fromWcOrder($wcOrder, $order); + $resultPurchaseUnit = current($result->purchaseUnits()); + $this->assertEquals($purchaseUnit, $resultPurchaseUnit); + } + + /** + * @dataProvider dataForTestFromPayPalResponseTest + * @param $orderData + */ + public function testFromPayPalResponse($orderData) + { + $purchaseUnitFactory = Mockery::mock(PurchaseUnitFactory::class); + if (count($orderData->purchase_units)) { + $purchaseUnitFactory + ->expects('fromPayPalResponse') + ->times(count($orderData->purchase_units)) + ->andReturn(Mockery::mock(PurchaseUnit::class)); + } + $payerFactory = Mockery::mock(PayerFactory::class); + if (isset($orderData->payer)) { + $payerFactory + ->expects('fromPayPalResponse') + ->andReturn(Mockery::mock(Payer::class)); + } + $applicationRepository = Mockery::mock(ApplicationContextRepository::class); + $applicationFactory = Mockery::mock(ApplicationContextFactory::class); + $paymentSourceFactory = Mockery::mock(PaymentSourceFactory::class); + + $testee = new OrderFactory( + $purchaseUnitFactory, + $payerFactory, + $applicationRepository, + $applicationFactory, + $paymentSourceFactory + ); + $order = $testee->fromPayPalResponse($orderData); + + $this->assertCount(count($orderData->purchase_units), $order->purchaseUnits()); + $this->assertEquals($orderData->id, $order->id()); + $this->assertEquals($orderData->status, $order->status()->name()); + $this->assertEquals($orderData->intent, $order->intent()); + if (! isset($orderData->create_time)) { + $this->assertNull($order->createTime()); + } else { + $this->assertEquals($orderData->create_time, $order->createTime()->format(\DateTime::ISO8601)); + } + if (! isset($orderData->payer)) { + $this->assertNull($order->payer()); + } else { + $this->assertInstanceOf(Payer::class, $order->payer()); + } + if (! isset($orderData->update_time)) { + $this->assertNull($order->updateTime()); + } else { + $this->assertEquals($orderData->update_time, $order->updateTime()->format(\DateTime::ISO8601)); + } + } + + public function dataForTestFromPayPalResponseTest() : array + { + return [ + 'default' => [ + (object) [ + 'id' => 'id', + 'purchase_units' => [new \stdClass(), new \stdClass()], + 'status' => OrderStatus::APPROVED, + 'intent' => 'CAPTURE', + 'create_time' => '2005-08-15T15:52:01+0000', + 'update_time' => '2005-09-15T15:52:01+0000', + 'payer' => new \stdClass(), + ], + ], + 'no_update_time' => [ + (object) [ + 'id' => 'id', + 'purchase_units' => [new \stdClass(), new \stdClass()], + 'status' => OrderStatus::APPROVED, + 'intent' => 'CAPTURE', + 'create_time' => '2005-08-15T15:52:01+0000', + 'payer' => new \stdClass(), + ], + ], + 'no_create_time' => [ + (object) [ + 'id' => 'id', + 'purchase_units' => [new \stdClass(), new \stdClass()], + 'status' => OrderStatus::APPROVED, + 'intent' => 'CAPTURE', + 'update_time' => '2005-09-15T15:52:01+0000', + 'payer' => new \stdClass(), + ], + ], + 'no_payer' => [ + (object) [ + 'id' => 'id', + 'purchase_units' => [new \stdClass(), new \stdClass()], + 'status' => OrderStatus::APPROVED, + 'intent' => 'CAPTURE', + 'create_time' => '2005-08-15T15:52:01+0000', + 'update_time' => '2005-09-15T15:52:01+0000', + ], + ], + ]; + } + + /** + * @dataProvider dataForTestFromPayPalResponseExceptionsTest + * @param $orderData + */ + public function testFromPayPalResponseExceptions($orderData) + { + $purchaseUnitFactory = Mockery::mock(PurchaseUnitFactory::class); + $payerFactory = Mockery::mock(PayerFactory::class); + $applicationRepository = Mockery::mock(ApplicationContextRepository::class); + $applicationFactory = Mockery::mock(ApplicationContextFactory::class); + $paymentSourceFactory = Mockery::mock(PaymentSourceFactory::class); + + $testee = new OrderFactory( + $purchaseUnitFactory, + $payerFactory, + $applicationRepository, + $applicationFactory, + $paymentSourceFactory + ); + + $this->expectException(RuntimeException::class); + $testee->fromPayPalResponse($orderData); + } + + public function dataForTestFromPayPalResponseExceptionsTest() : array + { + return [ + 'no_id' => [ + (object) [ + 'purchase_units' => [], + 'status' => '', + 'intent' => '', + ], + ], + 'no_purchase_units' => [ + (object) [ + 'id' => '', + 'status' => '', + 'intent' => '', + ], + ], + 'purchase_units_is_not_array' => [ + (object) [ + 'id' => '', + 'purchase_units' => 1, + 'status' => '', + 'intent' => '', + ], + ], + 'no_status' => [ + (object) [ + 'id' => '', + 'purchase_units' => [], + 'intent' => '', + ], + ], + 'no_intent' => [ + (object) [ + 'id' => '', + 'purchase_units' => [], + 'status' => '', + ], + ], + ]; + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/PayerFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/PayerFactoryTest.php new file mode 100644 index 000000000..44e16cbee --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/PayerFactoryTest.php @@ -0,0 +1,269 @@ +shouldReceive('get_billing_phone') + ->andReturn($expectedPhone); + $customer + ->shouldReceive('get_billing_email') + ->andReturn($expectedEmail); + $customer + ->shouldReceive('get_billing_last_name') + ->andReturn($expectedLastName); + $customer + ->shouldReceive('get_billing_first_name') + ->andReturn($expectedFirstName); + $addressFactory = Mockery::mock(AddressFactory::class); + $addressFactory + ->expects('fromWcCustomer') + ->with($customer, 'billing') + ->andReturn($address); + $testee = new PayerFactory($addressFactory); + $result = $testee->fromCustomer($customer); + + $this->assertEquals($expectedEmail, $result->emailAddress()); + $this->assertEquals($expectedLastName, $result->name()->surname()); + $this->assertEquals($expectedFirstName, $result->name()->givenName()); + $this->assertEquals($address, $result->address()); + $this->assertEquals($expectedPhone, $result->phone()->phone()->nationalNumber()); + $this->assertNull($result->birthDate()); + $this->assertEmpty($result->payerId()); + } + + /** + * The phone number is only allowed to contain numbers. + * The WC_Customer get_billing_phone can contain other characters, which need to + * get stripped. + */ + public function testFromWcCustomerStringsFromNumberAreRemoved() + { + $expectedPhone = '012345678901'; + $expectedEmail = 'test@example.com'; + $expectedFirstName = 'John'; + $expectedLastName = 'Locke'; + $address = Mockery::mock(Address::class); + $customer = Mockery::mock(\WC_Customer::class); + $customer + ->shouldReceive('get_billing_phone') + ->andReturn($expectedPhone . 'abcdefg'); + $customer + ->shouldReceive('get_billing_email') + ->andReturn($expectedEmail); + $customer + ->shouldReceive('get_billing_last_name') + ->andReturn($expectedLastName); + $customer + ->shouldReceive('get_billing_first_name') + ->andReturn($expectedFirstName); + $addressFactory = Mockery::mock(AddressFactory::class); + $addressFactory + ->expects('fromWcCustomer') + ->with($customer, 'billing') + ->andReturn($address); + $testee = new PayerFactory($addressFactory); + $result = $testee->fromCustomer($customer); + + $this->assertEquals($expectedPhone, $result->phone()->phone()->nationalNumber()); + } + + public function testFromWcCustomerNoNumber() + { + $expectedEmail = 'test@example.com'; + $expectedFirstName = 'John'; + $expectedLastName = 'Locke'; + $address = Mockery::mock(Address::class); + $customer = Mockery::mock(\WC_Customer::class); + $customer + ->shouldReceive('get_billing_phone') + ->andReturn(''); + $customer + ->shouldReceive('get_billing_email') + ->andReturn($expectedEmail); + $customer + ->shouldReceive('get_billing_last_name') + ->andReturn($expectedLastName); + $customer + ->shouldReceive('get_billing_first_name') + ->andReturn($expectedFirstName); + $addressFactory = Mockery::mock(AddressFactory::class); + $addressFactory + ->expects('fromWcCustomer') + ->with($customer, 'billing') + ->andReturn($address); + $testee = new PayerFactory($addressFactory); + $result = $testee->fromCustomer($customer); + + $this->assertNull($result->phone()); + } + + /** + * The phone number is not allowed to be longer than 14 characters. + * We need to make sure, we strip the number if longer. + */ + public function testFromWcCustomerTooLongNumberGetsStripped() + { + $expectedPhone = '01234567890123'; + $expectedEmail = 'test@example.com'; + $expectedFirstName = 'John'; + $expectedLastName = 'Locke'; + $address = Mockery::mock(Address::class); + $customer = Mockery::mock(\WC_Customer::class); + $customer + ->shouldReceive('get_billing_phone') + ->andReturn($expectedPhone . '456789'); + $customer + ->shouldReceive('get_billing_email') + ->andReturn($expectedEmail); + $customer + ->shouldReceive('get_billing_last_name') + ->andReturn($expectedLastName); + $customer + ->shouldReceive('get_billing_first_name') + ->andReturn($expectedFirstName); + $addressFactory = Mockery::mock(AddressFactory::class); + $addressFactory + ->expects('fromWcCustomer') + ->with($customer, 'billing') + ->andReturn($address); + $testee = new PayerFactory($addressFactory); + $result = $testee->fromCustomer($customer); + + $this->assertEquals($expectedPhone, $result->phone()->phone()->nationalNumber()); + } + + /** + * @dataProvider dataForTestFromPayPalResponse + */ + public function testFromPayPalResponse($data) + { + $addressFactory = Mockery::mock(AddressFactory::class); + $addressFactory + ->expects('fromPayPalRequest') + ->with($data->address) + ->andReturn(Mockery::mock(Address::class)); + $testee = new PayerFactory($addressFactory); + $payer = $testee->fromPayPalResponse($data); + $this->assertEquals($data->email_address, $payer->emailAddress()); + $this->assertEquals($data->payer_id, $payer->payerId()); + $this->assertEquals($data->name->given_name, $payer->name()->givenName()); + $this->assertEquals($data->name->surname, $payer->name()->surname()); + if (isset($data->phone)) { + $this->assertEquals($data->phone->phone_type, $payer->phone()->type()); + $this->assertEquals($data->phone->phone_number->national_number, $payer->phone()->phone()->nationalNumber()); + } else { + $this->assertNull($payer->phone()); + } + $this->assertInstanceOf(Address::class, $payer->address()); + if (isset($data->tax_info)) { + $this->assertEquals($data->tax_info->tax_id, $payer->taxInfo()->taxId()); + $this->assertEquals($data->tax_info->tax_id_type, $payer->taxInfo()->type()); + } else { + $this->assertNull($payer->taxInfo()); + } + if (isset($data->birth_date)) { + $this->assertEquals($data->birth_date, $payer->birthDate()->format('Y-m-d')); + } else { + $this->assertNull($payer->birthDate()); + } + } + + public function dataForTestFromPayPalResponse() : array + { + return [ + 'default' => [ + (object)[ + 'address' => new \stdClass(), + 'name' => (object)[ + 'given_name' => 'given_name', + 'surname' => 'surname', + ], + 'phone' => (object)[ + 'phone_type' => 'HOME', + 'phone_number' => (object)[ + 'national_number' => '1234567890', + ], + ], + 'tax_info' => (object)[ + 'tax_id' => 'tax_id', + 'tax_id_type' => 'BR_CPF', + ], + 'birth_date' => '1970-01-01', + 'email_address' => 'email_address', + 'payer_id' => 'payer_id', + ], + ], + 'no_phone' => [ + (object)[ + 'address' => new \stdClass(), + 'name' => (object)[ + 'given_name' => 'given_name', + 'surname' => 'surname', + ], + 'tax_info' => (object)[ + 'tax_id' => 'tax_id', + 'tax_id_type' => 'BR_CPF', + ], + 'birth_date' => '1970-01-01', + 'email_address' => 'email_address', + 'payer_id' => 'payer_id', + ], + ], + 'no_tax_info' => [ + (object)[ + 'address' => new \stdClass(), + 'name' => (object)[ + 'given_name' => 'given_name', + 'surname' => 'surname', + ], + 'phone' => (object)[ + 'phone_type' => 'HOME', + 'phone_number' => (object)[ + 'national_number' => '1234567890', + ], + ], + 'birth_date' => '1970-01-01', + 'email_address' => 'email_address', + 'payer_id' => 'payer_id', + ], + ], + 'no_birth_date' => [ + (object)[ + 'address' => new \stdClass(), + 'name' => (object)[ + 'given_name' => 'given_name', + 'surname' => 'surname', + ], + 'phone' => (object)[ + 'phone_type' => 'HOME', + 'phone_number' => (object)[ + 'national_number' => '1234567890', + ], + ], + 'tax_info' => (object)[ + 'tax_id' => 'tax_id', + 'tax_id_type' => 'BR_CPF', + ], + 'email_address' => 'email_address', + 'payer_id' => 'payer_id', + ], + ], + ]; + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php new file mode 100644 index 000000000..7ce333fd4 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/PaymentsFactoryTest.php @@ -0,0 +1,40 @@ +shouldReceive('toArray')->andReturn(['id' => 'foo', 'status' => 'CREATED']); + + $authorizationsFactory = Mockery::mock(AuthorizationFactory::class); + $authorizationsFactory->shouldReceive('fromPayPalRequest')->andReturn($authorization); + + $response = (object)[ + 'authorizations' => [ + (object)['id' => 'foo', 'status' => 'CREATED'], + ], + ]; + + $testee = new PaymentsFactory($authorizationsFactory); + $result = $testee->fromPayPalResponse($response); + + $this->assertInstanceOf(Payments::class, $result); + + $expectedToArray = [ + 'authorizations' => [ + ['id' => 'foo', 'status' => 'CREATED'], + ], + ]; + $this->assertEquals($expectedToArray, $result->toArray()); + } +} diff --git a/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php new file mode 100644 index 000000000..f044d92c1 --- /dev/null +++ b/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php @@ -0,0 +1,659 @@ +expects('get_id')->andReturn($wcOrderId); + $amount = Mockery::mock(Amount::class); + $amountFactory = Mockery::mock(AmountFactory::class); + $amountFactory + ->shouldReceive('fromWcOrder') + ->with($wcOrder) + ->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $payee = Mockery::mock(Payee::class); + $payeeRepository + ->shouldReceive('payee')->andReturn($payee); + $itemFactory = Mockery::mock(ItemFactory::class); + $itemFactory + ->shouldReceive('fromWcOrder') + ->with($wcOrder) + ->andReturn([]); + + $address = Mockery::mock(Address::class); + $address + ->shouldReceive('countryCode') + ->twice() + ->andReturn('DE'); + $address + ->shouldReceive('postalCode') + ->andReturn('12345'); + $shipping = Mockery::mock(Shipping::class); + $shipping + ->shouldReceive('address') + ->andReturn($address); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shippingFactory + ->shouldReceive('fromWcOrder') + ->with($wcOrder) + ->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $unit = $testee->fromWcOrder($wcOrder); + $this->assertTrue(is_a($unit, PurchaseUnit::class)); + $this->assertEquals($payee, $unit->payee()); + $this->assertEquals('', $unit->description()); + $this->assertEquals('default', $unit->referenceId()); + $this->assertEquals('WC-' . $wcOrderId, $unit->customId()); + $this->assertEquals('', $unit->softDescriptor()); + $this->assertEquals('WC-' . $wcOrderId, $unit->invoiceId()); + $this->assertEquals([], $unit->items()); + $this->assertEquals($amount, $unit->amount()); + $this->assertEquals($shipping, $unit->shipping()); + } + + public function testWcOrderShippingGetsDroppedWhenNoPostalCode() + { + $wcOrder = Mockery::mock(\WC_Order::class); + $wcOrder + ->expects('get_id')->andReturn(1); + $amount = Mockery::mock(Amount::class); + $amountFactory = Mockery::mock(AmountFactory::class); + $amountFactory + ->expects('fromWcOrder') + ->with($wcOrder) + ->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $payee = Mockery::mock(Payee::class); + $payeeRepository + ->expects('payee')->andReturn($payee); + $itemFactory = Mockery::mock(ItemFactory::class); + $itemFactory + ->expects('fromWcOrder') + ->with($wcOrder) + ->andReturn([]); + + $address = Mockery::mock(Address::class); + $address + ->expects('countryCode') + ->twice() + ->andReturn('DE'); + $address + ->expects('postalCode') + ->andReturn(''); + $shipping = Mockery::mock(Shipping::class); + $shipping + ->expects('address') + ->times(3) + ->andReturn($address); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shippingFactory + ->expects('fromWcOrder') + ->with($wcOrder) + ->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $unit = $testee->fromWcOrder($wcOrder); + $this->assertEquals(null, $unit->shipping()); + } + + public function testWcOrderShippingGetsDroppedWhenNoCountryCode() + { + $wcOrder = Mockery::mock(\WC_Order::class); + $wcOrder + ->expects('get_id')->andReturn(1); + $amount = Mockery::mock(Amount::class); + $amountFactory = Mockery::mock(AmountFactory::class); + $amountFactory + ->expects('fromWcOrder') + ->with($wcOrder) + ->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $payee = Mockery::mock(Payee::class); + $payeeRepository + ->expects('payee')->andReturn($payee); + $itemFactory = Mockery::mock(ItemFactory::class); + $itemFactory + ->expects('fromWcOrder') + ->with($wcOrder) + ->andReturn([]); + + $address = Mockery::mock(Address::class); + $address + ->expects('countryCode') + ->andReturn(''); + $shipping = Mockery::mock(Shipping::class); + $shipping + ->expects('address') + ->andReturn($address); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shippingFactory + ->expects('fromWcOrder') + ->with($wcOrder) + ->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $unit = $testee->fromWcOrder($wcOrder); + $this->assertEquals(null, $unit->shipping()); + } + + public function testWcCartDefault() + { + $wcCustomer = Mockery::mock(\WC_Customer::class); + expect('WC') + ->andReturn((object) ['customer' => $wcCustomer]); + + $wcCart = Mockery::mock(\WC_Cart::class); + $amount = Mockery::mock(Amount::class); + $amountFactory = Mockery::mock(AmountFactory::class); + $amountFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $payee = Mockery::mock(Payee::class); + $payeeRepository + ->expects('payee')->andReturn($payee); + $itemFactory = Mockery::mock(ItemFactory::class); + $itemFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn([]); + + $address = Mockery::mock(Address::class); + $address + ->shouldReceive('countryCode') + ->andReturn('DE'); + $address + ->shouldReceive('postalCode') + ->andReturn('12345'); + $shipping = Mockery::mock(Shipping::class); + $shipping + ->shouldReceive('address') + ->zeroOrMoreTimes() + ->andReturn($address); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shippingFactory + ->expects('fromWcCustomer') + ->with($wcCustomer) + ->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $unit = $testee->fromWcCart($wcCart); + $this->assertTrue(is_a($unit, PurchaseUnit::class)); + $this->assertEquals($payee, $unit->payee()); + $this->assertEquals('', $unit->description()); + $this->assertEquals('default', $unit->referenceId()); + $this->assertEquals('', $unit->customId()); + $this->assertEquals('', $unit->softDescriptor()); + $this->assertEquals('', $unit->invoiceId()); + $this->assertEquals([], $unit->items()); + $this->assertEquals($amount, $unit->amount()); + $this->assertEquals($shipping, $unit->shipping()); + } + + public function testWcCartShippingGetsDroppendWhenNoCustomer() + { + expect('WC') + ->andReturn((object) ['customer' => null]); + + $wcCart = Mockery::mock(\WC_Cart::class); + $amount = Mockery::mock(Amount::class); + $amountFactory = Mockery::mock(AmountFactory::class); + $amountFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $payee = Mockery::mock(Payee::class); + $payeeRepository + ->expects('payee')->andReturn($payee); + $itemFactory = Mockery::mock(ItemFactory::class); + $itemFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn([]); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $unit = $testee->fromWcCart($wcCart); + $this->assertNull($unit->shipping()); + } + + public function testWcCartShippingGetsDroppendWhenNoPostalCode() + { + expect('WC') + ->andReturn((object) ['customer' => Mockery::mock(\WC_Customer::class)]); + + $wcCart = Mockery::mock(\WC_Cart::class); + $amount = Mockery::mock(Amount::class); + $amountFactory = Mockery::mock(AmountFactory::class); + $amountFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $payee = Mockery::mock(Payee::class); + $payeeRepository + ->expects('payee')->andReturn($payee); + $itemFactory = Mockery::mock(ItemFactory::class); + $itemFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn([]); + + $address = Mockery::mock(Address::class); + $address + ->shouldReceive('countryCode') + ->andReturn('DE'); + $address + ->shouldReceive('postalCode') + ->andReturn(''); + $shipping = Mockery::mock(Shipping::class); + $shipping + ->shouldReceive('address') + ->andReturn($address); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shippingFactory + ->expects('fromWcCustomer') + ->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $unit = $testee->fromWcCart($wcCart); + $this->assertNull($unit->shipping()); + } + + public function testWcCartShippingGetsDroppendWhenNoCountryCode() + { + expect('WC') + ->andReturn((object) ['customer' => Mockery::mock(\WC_Customer::class)]); + + $wcCart = Mockery::mock(\WC_Cart::class); + $amount = Mockery::mock(Amount::class); + $amountFactory = Mockery::mock(AmountFactory::class); + $amountFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $payee = Mockery::mock(Payee::class); + $payeeRepository + ->expects('payee')->andReturn($payee); + $itemFactory = Mockery::mock(ItemFactory::class); + $itemFactory + ->expects('fromWcCart') + ->with($wcCart) + ->andReturn([]); + + $address = Mockery::mock(Address::class); + $address + ->shouldReceive('countryCode') + ->andReturn(''); + $shipping = Mockery::mock(Shipping::class); + $shipping + ->shouldReceive('address') + ->andReturn($address); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shippingFactory + ->expects('fromWcCustomer') + ->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $unit = $testee->fromWcCart($wcCart); + $this->assertNull($unit->shipping()); + } + + public function testFromPayPalResponseDefault() + { + $rawItem = (object) ['items' => 1]; + $rawAmount = (object) ['amount' => 1]; + $rawPayee = (object) ['payee' => 1]; + $rawShipping = (object) ['shipping' => 1]; + $amountFactory = Mockery::mock(AmountFactory::class); + $amount = Mockery::mock(Amount::class); + $amountFactory->expects('fromPayPalResponse')->with($rawAmount)->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payee = Mockery::mock(Payee::class); + $payeeFactory->expects('fromPayPalResponse')->with($rawPayee)->andReturn($payee); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $itemFactory = Mockery::mock(ItemFactory::class); + $item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]); + $itemFactory->expects('fromPayPalResponse')->with($rawItem)->andReturn($item); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shipping = Mockery::mock(Shipping::class); + $shippingFactory->expects('fromPayPalResponse')->with($rawShipping)->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $response = (object) [ + 'reference_id' => 'default', + 'description' => 'description', + 'custom_id' => 'customId', + 'invoice_id' => 'invoiceId', + 'soft_descriptor' => 'softDescriptor', + 'amount' => $rawAmount, + 'items' => [$rawItem], + 'payee' => $rawPayee, + 'shipping' => $rawShipping, + ]; + + $unit = $testee->fromPayPalResponse($response); + $this->assertTrue(is_a($unit, PurchaseUnit::class)); + $this->assertEquals($payee, $unit->payee()); + $this->assertEquals('description', $unit->description()); + $this->assertEquals('default', $unit->referenceId()); + $this->assertEquals('customId', $unit->customId()); + $this->assertEquals('softDescriptor', $unit->softDescriptor()); + $this->assertEquals('invoiceId', $unit->invoiceId()); + $this->assertEquals([$item], $unit->items()); + $this->assertEquals($amount, $unit->amount()); + $this->assertEquals($shipping, $unit->shipping()); + } + + public function testFromPayPalResponsePayeeIsNull() + { + $rawItem = (object) ['items' => 1]; + $rawAmount = (object) ['amount' => 1]; + $rawPayee = (object) ['payee' => 1]; + $rawShipping = (object) ['shipping' => 1]; + $amountFactory = Mockery::mock(AmountFactory::class); + $amount = Mockery::mock(Amount::class); + $amountFactory->expects('fromPayPalResponse')->with($rawAmount)->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $itemFactory = Mockery::mock(ItemFactory::class); + $item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]); + $itemFactory->expects('fromPayPalResponse')->with($rawItem)->andReturn($item); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shipping = Mockery::mock(Shipping::class); + $shippingFactory->expects('fromPayPalResponse')->with($rawShipping)->andReturn($shipping); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $response = (object) [ + 'reference_id' => 'default', + 'description' => 'description', + 'customId' => 'customId', + 'invoiceId' => 'invoiceId', + 'softDescriptor' => 'softDescriptor', + 'amount' => $rawAmount, + 'items' => [$rawItem], + 'shipping' => $rawShipping, + ]; + + $unit = $testee->fromPayPalResponse($response); + $this->assertNull($unit->payee()); + } + + public function testFromPayPalResponseShippingIsNull() + { + $rawItem = (object) ['items' => 1]; + $rawAmount = (object) ['amount' => 1]; + $rawPayee = (object) ['payee' => 1]; + $amountFactory = Mockery::mock(AmountFactory::class); + $amount = Mockery::mock(Amount::class); + $amountFactory->expects('fromPayPalResponse')->with($rawAmount)->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payee = Mockery::mock(Payee::class); + $payeeFactory->expects('fromPayPalResponse')->with($rawPayee)->andReturn($payee); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $itemFactory = Mockery::mock(ItemFactory::class); + $item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]); + $itemFactory->expects('fromPayPalResponse')->with($rawItem)->andReturn($item); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $response = (object) [ + 'reference_id' => 'default', + 'description' => 'description', + 'customId' => 'customId', + 'invoiceId' => 'invoiceId', + 'softDescriptor' => 'softDescriptor', + 'amount' => $rawAmount, + 'items' => [$rawItem], + 'payee' => $rawPayee, + ]; + + $unit = $testee->fromPayPalResponse($response); + $this->assertNull($unit->shipping()); + } + + public function testFromPayPalResponseNeedsReferenceId() + { + $amountFactory = Mockery::mock(AmountFactory::class); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $itemFactory = Mockery::mock(ItemFactory::class); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $paymentsFacory = Mockery::mock(PaymentsFactory::class); + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFacory + ); + + $response = (object) [ + 'description' => 'description', + 'customId' => 'customId', + 'invoiceId' => 'invoiceId', + 'softDescriptor' => 'softDescriptor', + 'amount' => '', + 'items' => [], + 'payee' => '', + 'shipping' => '', + ]; + + $this->expectException(\Inpsyde\PayPalCommerce\ApiClient\Exception\RuntimeException::class); + $testee->fromPayPalResponse($response); + } + + public function testFromPayPalResponsePaymentsGetAppended() + { + $rawItem = (object)['items' => 1]; + $rawAmount = (object)['amount' => 1]; + $rawPayee = (object)['payee' => 1]; + $rawShipping = (object)['shipping' => 1]; + $rawPayments = (object)['payments' => 1]; + + $amountFactory = Mockery::mock(AmountFactory::class); + $amount = Mockery::mock(Amount::class); + $amountFactory->expects('fromPayPalResponse')->with($rawAmount)->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payee = Mockery::mock(Payee::class); + $payeeFactory->expects('fromPayPalResponse')->with($rawPayee)->andReturn($payee); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $itemFactory = Mockery::mock(ItemFactory::class); + $item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]); + $itemFactory->expects('fromPayPalResponse')->with($rawItem)->andReturn($item); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shipping = Mockery::mock(Shipping::class); + $shippingFactory->expects('fromPayPalResponse')->with($rawShipping)->andReturn($shipping); + + $paymentsFactory = Mockery::mock(PaymentsFactory::class); + $payments = Mockery::mock(Payments::class); + $paymentsFactory->expects('fromPayPalResponse')->with($rawPayments)->andReturn($payments); + + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFactory + ); + + $response = (object)[ + 'reference_id' => 'default', + 'description' => 'description', + 'customId' => 'customId', + 'invoiceId' => 'invoiceId', + 'softDescriptor' => 'softDescriptor', + 'amount' => $rawAmount, + 'items' => [$rawItem], + 'payee' => $rawPayee, + 'shipping' => $rawShipping, + 'payments' => $rawPayments, + ]; + + $unit = $testee->fromPayPalResponse($response); + $this->assertEquals($payments, $unit->payments()); + } + + public function testFromPayPalResponsePaymentsIsNull() + { + $rawItem = (object)['items' => 1]; + $rawAmount = (object)['amount' => 1]; + $rawPayee = (object)['payee' => 1]; + $rawShipping = (object)['shipping' => 1]; + $rawPayments = (object)['payments' => 1]; + + $amountFactory = Mockery::mock(AmountFactory::class); + $amount = Mockery::mock(Amount::class); + $amountFactory->expects('fromPayPalResponse')->with($rawAmount)->andReturn($amount); + $payeeFactory = Mockery::mock(PayeeFactory::class); + $payee = Mockery::mock(Payee::class); + $payeeFactory->expects('fromPayPalResponse')->with($rawPayee)->andReturn($payee); + $payeeRepository = Mockery::mock(PayeeRepository::class); + $itemFactory = Mockery::mock(ItemFactory::class); + $item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]); + $itemFactory->expects('fromPayPalResponse')->with($rawItem)->andReturn($item); + $shippingFactory = Mockery::mock(ShippingFactory::class); + $shipping = Mockery::mock(Shipping::class); + $shippingFactory->expects('fromPayPalResponse')->with($rawShipping)->andReturn($shipping); + + $paymentsFactory = Mockery::mock(PaymentsFactory::class); + + $testee = new PurchaseUnitFactory( + $amountFactory, + $payeeRepository, + $payeeFactory, + $itemFactory, + $shippingFactory, + $paymentsFactory + ); + + $response = (object)[ + 'reference_id' => 'default', + 'description' => 'description', + 'customId' => 'customId', + 'invoiceId' => 'invoiceId', + 'softDescriptor' => 'softDescriptor', + 'amount' => $rawAmount, + 'items' => [$rawItem], + 'payee' => $rawPayee, + 'shipping' => $rawShipping, + ]; + + $unit = $testee->fromPayPalResponse($response); + $this->assertNull($unit->payments()); + } +} diff --git a/tests/PHPUnit/ApiClient/Repository/PayeeRepositoryTest.php b/tests/PHPUnit/ApiClient/Repository/PayeeRepositoryTest.php new file mode 100644 index 000000000..0648597ee --- /dev/null +++ b/tests/PHPUnit/ApiClient/Repository/PayeeRepositoryTest.php @@ -0,0 +1,22 @@ +payee(); + $this->assertEquals($merchantId, $payee->merchantId()); + $this->assertEquals($merchantEmail, $payee->email()); + } +} diff --git a/tests/PHPUnit/ApiClient/TestCase.php b/tests/PHPUnit/ApiClient/TestCase.php new file mode 100644 index 000000000..ca5045c2d --- /dev/null +++ b/tests/PHPUnit/ApiClient/TestCase.php @@ -0,0 +1,28 @@ +andReturnUsing(function (string $text) { + return $text; + }); + setUp(); + } + + public function tearDown(): void + { + tearDown(); + Mockery::close(); + parent::tearDown(); + } +}