From b798ccc0e9ea3a22b0652f5d4fb1bc7524d78126 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 14 Mar 2023 16:37:29 +0200 Subject: [PATCH 01/23] Store container in a globally accessible field needed for providing API --- src/PPCP.php | 48 +++++++++++++++++++++++++++ tests/e2e/PHPUnit/TestCase.php | 4 +-- tests/e2e/PHPUnit/ppcp-e2e-plugin.php | 19 ----------- tests/e2e/PHPUnit/setup.php | 12 ------- woocommerce-paypal-payments.php | 2 ++ 5 files changed, 52 insertions(+), 33 deletions(-) create mode 100644 src/PPCP.php delete mode 100644 tests/e2e/PHPUnit/ppcp-e2e-plugin.php diff --git a/src/PPCP.php b/src/PPCP.php new file mode 100644 index 000000000..ff505d2aa --- /dev/null +++ b/src/PPCP.php @@ -0,0 +1,48 @@ + $value) { update_option($key, $value); } -echo 'Adding ppcp-e2e-plugin.' . PHP_EOL; - -$pluginDir = WP_ROOT_DIR . '/wp-content/plugins/ppcp-e2e-plugin'; -if (!is_dir($pluginDir)) { - mkdir($pluginDir); -} -if (!copy(E2E_TESTS_ROOT_DIR . '/PHPUnit/ppcp-e2e-plugin.php', $pluginDir . '/ppcp-e2e-plugin.php')) { - echo 'Failed to copy ppcp-e2e-plugin.' . PHP_EOL; -} - -activate_plugin('ppcp-e2e-plugin/ppcp-e2e-plugin.php', '', true); - echo 'Deleting test taxes.' . PHP_EOL; $taxRates = WC_Tax::get_rates_for_tax_class(''); diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 52766dccf..4b1b85edf 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -74,6 +74,8 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); $app_container = $bootstrap( $root_dir ); + PPCP::init( $app_container ); + $initialized = true; /** * The hook fired after the plugin bootstrap with the app services container as parameter. From b62072c53738536bcc0cb64b9de5ebb18d346bb6 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 14 Mar 2023 16:38:28 +0200 Subject: [PATCH 02/23] Add missing files to psalm --- .psalm/stubs.php | 4 ++++ bootstrap.php | 5 +++++ psalm.xml.dist | 2 ++ src/FilePathPluginFactory.php | 5 ++--- woocommerce-paypal-payments.php | 27 ++++++++++++++++++++++++--- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.psalm/stubs.php b/.psalm/stubs.php index bdf5e45c5..8b4990022 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -9,6 +9,10 @@ if (!defined('HOUR_IN_SECONDS')) { define('HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS); } +if (!defined('ABSPATH')) { + define('ABSPATH', ''); +} + /** * Cancel the next occurrence of a scheduled action. * diff --git a/bootstrap.php b/bootstrap.php index 7b8f7271d..8d4514e41 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -19,6 +19,11 @@ return function ( array $additional_containers = array(), array $additional_modules = array() ): ContainerInterface { + /** + * Skip path check. + * + * @psalm-suppress UnresolvableInclude + */ $modules = ( require "$root_dir/modules.php" )( $root_dir ); $modules = array_merge( $modules, $additional_modules ); diff --git a/psalm.xml.dist b/psalm.xml.dist index fffe2fa98..d6d4eaf98 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -25,6 +25,8 @@ + + diff --git a/src/FilePathPluginFactory.php b/src/FilePathPluginFactory.php index da688ad6e..69d118fbb 100644 --- a/src/FilePathPluginFactory.php +++ b/src/FilePathPluginFactory.php @@ -58,10 +58,9 @@ class FilePathPluginFactory implements FilePathPluginFactoryInterface { if ( ! function_exists( 'get_plugin_data' ) ) { /** - * Skip check for WP files and constants. + * Skip check for WP files. * - * @psalm-suppress UnresolvableInclude - * @psalm-suppress UndefinedConstant + * @psalm-suppress MissingFile */ require_once ABSPATH . 'wp-admin/includes/plugin.php'; } diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 4b1b85edf..7b97e3f6e 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -43,7 +43,7 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); /** * Initialize the plugin and its modules. */ - function init() { + function init(): void { $root_dir = __DIR__; if ( ! is_woocommerce_activated() ) { @@ -90,6 +90,11 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); init(); if ( ! function_exists( 'get_plugin_data' ) ) { + /** + * Skip check for WP files. + * + * @psalm-suppress MissingFile + */ require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugin_data = get_plugin_data( __DIR__ . '/woocommerce-paypal-payments.php' ); @@ -132,9 +137,14 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); } ); - // Add "Settings" link to Plugins screen. add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), + /** + * Add "Settings" link to Plugins screen. + * + * @param array $links + * @retun array + */ function( $links ) { if ( ! is_woocommerce_activated() ) { return $links; @@ -153,9 +163,15 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); } ); - // Add links below the description on the Plugins page. add_filter( 'plugin_row_meta', + /** + * Add links below the description on the Plugins page. + * + * @param array $links + * @param string $file + * @retun array + */ function( $links, $file ) { if ( plugin_basename( __FILE__ ) !== $file ) { return $links; @@ -195,6 +211,11 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); 'before_woocommerce_init', function() { if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) { + /** + * Skip WC class check. + * + * @psalm-suppress UndefinedClass + */ \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); } } From d40d01a4f45afc6dfcb459949c33a257aff52445 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 14 Mar 2023 16:40:20 +0200 Subject: [PATCH 03/23] Add API for easy programmatic capture --- api/order-functions.php | 46 ++ composer.json | 5 +- composer.lock | 483 ++++++++++++++++-- phpcs.xml.dist | 1 + psalm.xml.dist | 1 + tests/PHPUnit/Api/OrderCaptureTest.php | 92 ++++ .../ApiClient/Endpoint/IdentityTokenTest.php | 2 +- tests/PHPUnit/ModularTestCase.php | 2 + tests/PHPUnit/TestCase.php | 3 + 9 files changed, 577 insertions(+), 58 deletions(-) create mode 100644 api/order-functions.php create mode 100644 tests/PHPUnit/Api/OrderCaptureTest.php diff --git a/api/order-functions.php b/api/order-functions.php new file mode 100644 index 000000000..cb15478de --- /dev/null +++ b/api/order-functions.php @@ -0,0 +1,46 @@ +get_meta( PayPalGateway::INTENT_META_KEY ) ); + + if ( $intent !== 'AUTHORIZE' ) { + throw new InvalidArgumentException( 'Only orders with "authorize" intent can be captured.' ); + } + $captured = wc_string_to_bool( $wc_order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ) ); + if ( $captured ) { + throw new InvalidArgumentException( 'The order is already captured.' ); + } + + $authorized_payment_processor = PPCP::container()->get( 'wcgateway.processor.authorized-payments' ); + assert( $authorized_payment_processor instanceof AuthorizedPaymentsProcessor ); + + if ( ! $authorized_payment_processor->capture_authorized_payment( $wc_order ) ) { + throw new RuntimeException( 'Capture failed.' ); + } +} diff --git a/composer.json b/composer.json index e149ce2ed..4a94eb54b 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,10 @@ "psr-4": { "WooCommerce\\PayPalCommerce\\": "src", "WooCommerce\\PayPalCommerce\\Vendor\\": "lib/packages/" - } + }, + "files": [ + "api/order-functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index ce4259243..295f011ee 100644 --- a/composer.lock +++ b/composer.lock @@ -35,6 +35,10 @@ ], "description": "Promoting container interoperability through standard service providers", "homepage": "https://github.com/container-interop/service-provider", + "support": { + "issues": "https://github.com/container-interop/service-provider/issues", + "source": "https://github.com/container-interop/service-provider/tree/master" + }, "time": "2017-09-20T14:13:36+00:00" }, { @@ -86,6 +90,10 @@ } ], "description": "Interfaces for human readable string interoperation.", + "support": { + "issues": "https://github.com/Dhii/human-readable-interface/issues", + "source": "https://github.com/Dhii/human-readable-interface/tree/v0.2.0-alpha1" + }, "time": "2021-03-05T00:36:01+00:00" }, { @@ -134,6 +142,10 @@ } ], "description": "Interfaces for modules", + "support": { + "issues": "https://github.com/Dhii/module-interface/issues", + "source": "https://github.com/Dhii/module-interface/tree/v0.3.0-alpha2" + }, "time": "2021-08-23T08:23:01+00:00" }, { @@ -177,6 +189,10 @@ } ], "description": "Interfaces for package-related interop", + "support": { + "issues": "https://github.com/Dhii/package-interface/issues", + "source": "https://github.com/Dhii/package-interface/tree/v0.1.0-alpha4" + }, "time": "2021-12-08T15:57:36+00:00" }, { @@ -225,6 +241,10 @@ } ], "description": "A base interface for validators", + "support": { + "issues": "https://github.com/Dhii/validation-interface/issues", + "source": "https://github.com/Dhii/validation-interface/tree/v0.3.0-alpha3" + }, "time": "2021-01-14T16:19:20+00:00" }, { @@ -268,6 +288,10 @@ } ], "description": "Implementation for dealing with SemVer-compliant versions", + "support": { + "issues": "https://github.com/Dhii/versions/issues", + "source": "https://github.com/Dhii/versions/tree/v0.1.0-alpha3" + }, "time": "2021-12-08T16:54:50+00:00" }, { @@ -312,6 +336,10 @@ "container-interop", "psr" ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, "time": "2021-03-05T17:36:06+00:00" }, { @@ -359,6 +387,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, "time": "2021-05-03T11:20:27+00:00" }, { @@ -399,6 +430,10 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { @@ -465,6 +500,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -528,6 +566,10 @@ } ], "description": "Composer plugin to merge multiple composer.json files", + "support": { + "issues": "https://github.com/wikimedia/composer-merge-plugin/issues", + "source": "https://github.com/wikimedia/composer-merge-plugin/tree/v2.0.1" + }, "time": "2021-02-24T05:28:06+00:00" }, { @@ -576,6 +618,10 @@ } ], "description": "Interfaces for interop within WordPress", + "support": { + "issues": "https://github.com/wp-oop/wordpress-interface/issues", + "source": "https://github.com/wp-oop/wordpress-interface/tree/v0.1.0-alpha2" + }, "time": "2021-04-30T09:37:37+00:00" } ], @@ -656,6 +702,11 @@ "non-blocking", "promise" ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.2" + }, "funding": [ { "url": "https://github.com/amphp", @@ -697,12 +748,12 @@ } }, "autoload": { - "psr-4": { - "Amp\\ByteStream\\": "lib" - }, "files": [ "lib/functions.php" - ] + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -728,6 +779,11 @@ "non-blocking", "stream" ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + }, "funding": [ { "url": "https://github.com/amphp", @@ -738,16 +794,16 @@ }, { "name": "antecedent/patchwork", - "version": "2.1.21", + "version": "2.1.25", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d" + "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d", - "reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/17314e042d45e0dacb0a494c2d1ef50e7621136a", + "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a", "shasum": "" }, "require": { @@ -778,7 +834,11 @@ "runkit", "testing" ], - "time": "2022-02-07T07:28:34+00:00" + "support": { + "issues": "https://github.com/antecedent/patchwork/issues", + "source": "https://github.com/antecedent/patchwork/tree/2.1.25" + }, + "time": "2023-02-19T12:51:24+00:00" }, { "name": "brain/monkey", @@ -844,6 +904,10 @@ "test", "testing" ], + "support": { + "issues": "https://github.com/Brain-WP/BrainMonkey/issues", + "source": "https://github.com/Brain-WP/BrainMonkey" + }, "time": "2021-11-11T15:53:55+00:00" }, { @@ -899,6 +963,10 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" + }, "funding": [ { "url": "https://packagist.com", @@ -966,6 +1034,10 @@ "regex", "regular expression" ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/2.1.0" + }, "funding": [ { "url": "https://packagist.com", @@ -1042,6 +1114,11 @@ "validation", "versioning" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, "funding": [ { "url": "https://packagist.com", @@ -1103,6 +1180,11 @@ "Xdebug", "performance" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, "funding": [ { "url": "https://packagist.com", @@ -1188,6 +1270,10 @@ "stylecheck", "tests" ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, "time": "2022-02-04T12:51:07+00:00" }, { @@ -1239,6 +1325,10 @@ } ], "description": "A highly ISP-compliant collection of interfaces that represent maps and lists.", + "support": { + "issues": "https://github.com/Dhii/collections-interface/issues", + "source": "https://github.com/Dhii/collections-interface/tree/v0.3.0" + }, "time": "2021-10-06T10:56:09+00:00" }, { @@ -1295,6 +1385,10 @@ "PSR-11", "container" ], + "support": { + "issues": "https://github.com/Dhii/containers/issues", + "source": "https://github.com/Dhii/containers/tree/v0.1.4" + }, "time": "2021-10-06T11:13:51+00:00" }, { @@ -1328,6 +1422,10 @@ "MIT" ], "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, "time": "2019-12-04T15:06:13+00:00" }, { @@ -1380,6 +1478,10 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -1435,6 +1537,10 @@ } ], "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, "time": "2021-06-11T22:34:44+00:00" }, { @@ -1487,28 +1593,32 @@ "php", "server" ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" + }, "time": "2022-03-02T22:36:06+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "a878d45c1914464426dc94da61c9e1d36ae262a8" + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/a878d45c1914464426dc94da61c9e1d36ae262a8", - "reference": "a878d45c1914464426dc94da61c9e1d36ae262a8", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9" + "phpoption/phpoption": "^1.9.1" }, "require-dev": { - "phpunit/phpunit": "^8.5.28 || ^9.5.21" + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" }, "type": "library", "autoload": { @@ -1535,6 +1645,10 @@ "Result-Type", "result" ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -1545,7 +1659,7 @@ "type": "tidelift" } ], - "time": "2022-07-30T15:56:11+00:00" + "time": "2023-02-25T20:23:15+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -1592,6 +1706,10 @@ "keywords": [ "test" ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + }, "time": "2020-07-09T08:09:16+00:00" }, { @@ -1657,20 +1775,24 @@ "test double", "testing" ], + "support": { + "issues": "https://github.com/mockery/mockery/issues", + "source": "https://github.com/mockery/mockery/tree/1.3.6" + }, "time": "2022-09-07T15:05:49+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -1706,13 +1828,17 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "netresearch/jsonmapper", @@ -1758,20 +1884,25 @@ } ], "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.1.0" + }, "time": "2022-12-08T20:46:14+00:00" }, { "name": "nikic/php-parser", - "version": "v4.15.3", + "version": "v4.15.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", "shasum": "" }, "require": { @@ -1810,7 +1941,11 @@ "parser", "php" ], - "time": "2023-01-16T22:05:37+00:00" + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + }, + "time": "2023-03-05T19:49:14+00:00" }, { "name": "openlss/lib-array2xml", @@ -1859,6 +1994,10 @@ "xml", "xml conversion" ], + "support": { + "issues": "https://github.com/nullivex/lib-array2xml/issues", + "source": "https://github.com/nullivex/lib-array2xml/tree/master" + }, "time": "2019-03-29T20:06:56+00:00" }, { @@ -1915,6 +2054,10 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, "time": "2021-07-20T11:28:43+00:00" }, { @@ -1962,6 +2105,10 @@ } ], "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, "time": "2022-02-21T01:04:05+00:00" }, { @@ -2002,6 +2149,10 @@ "woocommerce", "wordpress" ], + "support": { + "issues": "https://github.com/php-stubs/woocommerce-stubs/issues", + "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v5.9.1" + }, "time": "2022-04-30T06:35:48+00:00" }, { @@ -2045,6 +2196,10 @@ "static analysis", "wordpress" ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.5" + }, "time": "2022-11-09T05:32:14+00:00" }, { @@ -2103,6 +2258,10 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, "time": "2019-12-27T09:44:58+00:00" }, { @@ -2156,6 +2315,10 @@ "standards", "static analysis" ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, "time": "2022-10-25T01:46:02+00:00" }, { @@ -2207,6 +2370,10 @@ "static analysis", "wordpress" ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, "time": "2022-10-24T09:00:36+00:00" }, { @@ -2256,6 +2423,10 @@ "reflection", "static analysis" ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, "time": "2020-06-27T09:03:43+00:00" }, { @@ -2309,6 +2480,10 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, "time": "2021-10-19T17:43:47+00:00" }, { @@ -2355,28 +2530,32 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" + }, "time": "2022-03-15T21:29:03+00:00" }, { "name": "phpoption/phpoption", - "version": "1.9.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab" + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dc5ff11e274a90cc1c743f66c9ad700ce50db9ab", - "reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "phpunit/phpunit": "^8.5.28 || ^9.5.21" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" }, "type": "library", "extra": { @@ -2416,6 +2595,10 @@ "php", "type" ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -2426,7 +2609,7 @@ "type": "tidelift" } ], - "time": "2022-07-30T15:51:26+00:00" + "time": "2023-02-25T19:38:58+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2489,6 +2672,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.15" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2545,6 +2732,10 @@ "filesystem", "iterator" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.5" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2592,6 +2783,10 @@ "keywords": [ "template" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, "time": "2015-06-21T13:50:34+00:00" }, { @@ -2641,6 +2836,16 @@ "keywords": [ "timer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T08:20:02+00:00" }, { @@ -2690,21 +2895,31 @@ "keywords": [ "tokenizer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "abandoned": true, "time": "2021-07-26T12:15:06+00:00" }, { "name": "phpunit/phpunit", - "version": "8.5.31", + "version": "8.5.33", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "33c126b09a42de5c99e5e8032b54e8221264a74e" + "reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33c126b09a42de5c99e5e8032b54e8221264a74e", - "reference": "33c126b09a42de5c99e5e8032b54e8221264a74e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", + "reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e", "shasum": "" }, "require": { @@ -2770,6 +2985,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.33" + }, "funding": [ { "url": "https://phpunit.de/sponsors.html", @@ -2784,7 +3003,7 @@ "type": "tidelift" } ], - "time": "2022-10-28T05:57:37+00:00" + "time": "2023-02-27T13:04:50+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2829,6 +3048,16 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T08:15:22+00:00" }, { @@ -2893,6 +3122,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2955,6 +3188,16 @@ "unidiff", "unified diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T07:59:04+00:00" }, { @@ -3008,6 +3251,16 @@ "environment", "hhvm" ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T07:53:42+00:00" }, { @@ -3075,6 +3328,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3135,6 +3392,10 @@ "keywords": [ "global state" ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3188,6 +3449,16 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T07:40:27+00:00" }, { @@ -3233,6 +3504,16 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T07:37:18+00:00" }, { @@ -3286,6 +3567,16 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T07:34:24+00:00" }, { @@ -3328,6 +3619,16 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-11-30T07:30:19+00:00" }, { @@ -3374,6 +3675,10 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/1.1.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3423,20 +3728,24 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, "time": "2016-10-03T07:35:21+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "version": "3.7.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", "shasum": "" }, "require": { @@ -3472,22 +3781,28 @@ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], - "time": "2022-06-18T07:21:10+00:00" + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2023-02-22T23:07:41+00:00" }, { "name": "symfony/console", - "version": "v5.4.17", + "version": "v5.4.21", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f" + "reference": "c77433ddc6cdc689caf48065d9ea22ca0853fbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", + "url": "https://api.github.com/repos/symfony/console/zipball/c77433ddc6cdc689caf48065d9ea22ca0853fbd9", + "reference": "c77433ddc6cdc689caf48065d9ea22ca0853fbd9", "shasum": "" }, "require": { @@ -3556,6 +3871,9 @@ "console", "terminal" ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.21" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3570,7 +3888,7 @@ "type": "tidelift" } ], - "time": "2022-12-28T14:15:31+00:00" + "time": "2023-02-25T16:59:41+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3620,6 +3938,9 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3699,6 +4020,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3777,6 +4101,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3858,6 +4185,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3938,6 +4268,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4014,6 +4347,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4094,6 +4430,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4112,16 +4451,16 @@ }, { "name": "symfony/string", - "version": "v5.4.17", + "version": "v5.4.21", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b" + "reference": "edac10d167b78b1d90f46a80320d632de0bd9f2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/55733a8664b8853b003e70251c58bc8cb2d82a6b", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b", + "url": "https://api.github.com/repos/symfony/string/zipball/edac10d167b78b1d90f46a80320d632de0bd9f2f", + "reference": "edac10d167b78b1d90f46a80320d632de0bd9f2f", "shasum": "" }, "require": { @@ -4177,6 +4516,9 @@ "utf-8", "utf8" ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.21" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4191,7 +4533,7 @@ "type": "tidelift" } ], - "time": "2022-12-12T15:54:21+00:00" + "time": "2023-02-22T08:00:55+00:00" }, { "name": "theseer/tokenizer", @@ -4231,6 +4573,10 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, "funding": [ { "url": "https://github.com/theseer", @@ -4341,6 +4687,10 @@ "inspection", "php" ], + "support": { + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm/tree/4.30.0" + }, "time": "2022-11-06T20:37:08+00:00" }, { @@ -4411,6 +4761,10 @@ "env", "environment" ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -4475,6 +4829,10 @@ "check", "validate" ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, "time": "2022-06-03T18:03:27+00:00" }, { @@ -4521,6 +4879,10 @@ } ], "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "issues": "https://github.com/webmozart/path-util/issues", + "source": "https://github.com/webmozart/path-util/tree/2.3.0" + }, "abandoned": "symfony/filesystem", "time": "2015-12-17T08:42:14+00:00" }, @@ -4562,6 +4924,10 @@ "woocommerce", "wordpress" ], + "support": { + "issues": "https://github.com/woocommerce/woocommerce-sniffs/issues", + "source": "https://github.com/woocommerce/woocommerce-sniffs/tree/0.1.3" + }, "time": "2022-02-17T15:34:51+00:00" }, { @@ -4608,6 +4974,11 @@ "standards", "wordpress" ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, "time": "2020-05-13T23:57:56+00:00" } ], @@ -4620,9 +4991,9 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0, ^7.2 | ^8.0", + "php": "^7.2 | ^8.0", "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.3.0" } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index d12770d98..e7c5693ae 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -42,6 +42,7 @@ + api src modules woocommerce-paypal-payments.php diff --git a/psalm.xml.dist b/psalm.xml.dist index d6d4eaf98..4be6e9690 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -23,6 +23,7 @@ errorBaseline="psalm-baseline.xml" > + diff --git a/tests/PHPUnit/Api/OrderCaptureTest.php b/tests/PHPUnit/Api/OrderCaptureTest.php new file mode 100644 index 000000000..4e9dd82ce --- /dev/null +++ b/tests/PHPUnit/Api/OrderCaptureTest.php @@ -0,0 +1,92 @@ +authorizedPaymentProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class); + + $this->bootstrapModule([ + 'wcgateway.processor.authorized-payments' => function () { + return $this->authorizedPaymentProcessor; + }, + ]); + } + + public function testSuccess(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::INTENT_META_KEY) + ->andReturn('AUTHORIZE'); + $wcOrder->expects('get_meta') + ->with(AuthorizedPaymentsProcessor::CAPTURED_META_KEY) + ->andReturn(false); + + $this->authorizedPaymentProcessor + ->expects('capture_authorized_payment') + ->andReturnTrue() + ->once(); + + ppcp_capture_order($wcOrder); + } + + public function testFailure(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::INTENT_META_KEY) + ->andReturn('AUTHORIZE'); + $wcOrder->expects('get_meta') + ->with(AuthorizedPaymentsProcessor::CAPTURED_META_KEY) + ->andReturn(false); + + $this->authorizedPaymentProcessor + ->expects('capture_authorized_payment') + ->andReturnFalse() + ->once(); + + $this->expectException(RuntimeException::class); + + ppcp_capture_order($wcOrder); + } + + public function testNotAuthorize(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->shouldReceive('get_meta') + ->with(PayPalGateway::INTENT_META_KEY) + ->andReturn('CAPTURE'); + $wcOrder->shouldReceive('get_meta') + ->with(AuthorizedPaymentsProcessor::CAPTURED_META_KEY) + ->andReturn(false); + + $this->expectException(InvalidArgumentException::class); + + ppcp_capture_order($wcOrder); + } + + public function testAlreadyCaptured(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->shouldReceive('get_meta') + ->with(PayPalGateway::INTENT_META_KEY) + ->andReturn('CAPTURE'); + $wcOrder->shouldReceive('get_meta') + ->with(AuthorizedPaymentsProcessor::CAPTURED_META_KEY) + ->andReturn(true); + + $this->expectException(InvalidArgumentException::class); + + ppcp_capture_order($wcOrder); + } +} diff --git a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php index 6b0b2ef14..fe66ab43f 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/IdentityTokenTest.php @@ -47,7 +47,7 @@ class IdentityTokenTest extends TestCase public function testGenerateForCustomerReturnsToken() { $id = 1; - define( 'PPCP_FLAG_SUBSCRIPTION', true ); + !defined('PPCP_FLAG_SUBSCRIPTION') && define('PPCP_FLAG_SUBSCRIPTION', true); $token = Mockery::mock(Token::class); $token ->expects('token')->andReturn('bearer'); diff --git a/tests/PHPUnit/ModularTestCase.php b/tests/PHPUnit/ModularTestCase.php index 3276d1d80..8287708ac 100644 --- a/tests/PHPUnit/ModularTestCase.php +++ b/tests/PHPUnit/ModularTestCase.php @@ -82,6 +82,8 @@ class ModularTestCase extends TestCase $bootstrap = require ("$rootDir/bootstrap.php"); $appContainer = $bootstrap($rootDir, [], [$module]); + PPCP::init($appContainer); + return $appContainer; } } diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php index f5a480ddc..f13497885 100644 --- a/tests/PHPUnit/TestCase.php +++ b/tests/PHPUnit/TestCase.php @@ -28,6 +28,9 @@ class TestCase extends \PHPUnit\Framework\TestCase when('wc_print_r')->alias(function ($value, bool $return = false) { return print_r($value, $return); }); + when('wc_string_to_bool')->alias(function ($string) { + return is_bool( $string ) ? $string : ( 'yes' === strtolower( $string ) || 1 === $string || 'true' === strtolower( $string ) || '1' === $string ); + }); when('get_plugin_data')->justReturn(['Version' => '1.0']); when('plugin_basename')->justReturn('woocommerce-paypal-payments/woocommerce-paypal-payments.php'); From e417fcb1f948da03b2d0d434d1ac43eead7fcf48 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 17 Mar 2023 10:20:23 +0200 Subject: [PATCH 04/23] Fix psalm error --- bootstrap.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bootstrap.php b/bootstrap.php index 8d4514e41..fe4552110 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -46,7 +46,12 @@ return function ( // TODO: caching does not work currently, // may want to consider fixing it later (pass proxy as parent to DelegatingContainer) // for now not fixed since we were using this behavior for long time and fixing it now may break things. - $container = new DelegatingContainer( $provider ); + $container = new DelegatingContainer( $provider ); + /** + * Skip iterable vs array check. + * + * @psalm-suppress PossiblyInvalidArgument + */ $app_container = new CachingContainer( new CompositeContainer( array_merge( From 9ba93c9e116dd3cf7535802ad5497e138afeba4e Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 17 Mar 2023 10:22:12 +0200 Subject: [PATCH 05/23] Remove unneeded extension no reason to put it into extension, only adds confusion --- modules/ppcp-api-client/services.php | 17 +++++++----- modules/ppcp-wc-gateway/extensions.php | 38 -------------------------- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index e2ad9c05d..1b1eb3da0 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient; +use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; @@ -177,12 +178,13 @@ return array( $patch_collection_factory = $container->get( 'api.factory.patch-collection-factory' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); - /** - * The settings. - * - * @var Settings $settings - */ - $settings = $container->get( 'wcgateway.settings' ); + $session_handler = $container->get( 'session.handler' ); + assert( $session_handler instanceof SessionHandler ); + $bn_code = $session_handler->bn_code(); + + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + $intent = $settings->has( 'intent' ) && strtoupper( (string) $settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE'; $application_context_repository = $container->get( 'api.repository.application-context' ); $subscription_helper = $container->get( 'subscription.helper' ); @@ -196,7 +198,8 @@ return array( $application_context_repository, $subscription_helper, $container->get( 'wcgateway.is-fraudnet-enabled' ), - $container->get( 'wcgateway.fraudnet' ) + $container->get( 'wcgateway.fraudnet' ), + $bn_code ); }, 'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint { diff --git a/modules/ppcp-wc-gateway/extensions.php b/modules/ppcp-wc-gateway/extensions.php index 363d12292..7b3c1b127 100644 --- a/modules/ppcp-wc-gateway/extensions.php +++ b/modules/ppcp-wc-gateway/extensions.php @@ -9,10 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Environment; -use WooCommerce\PayPalCommerce\Session\SessionHandler; -use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; use WooCommerce\WooCommerce\Logging\Logger\WooCommerceLogger; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; @@ -52,41 +49,6 @@ return array( $settings = $container->get( 'wcgateway.settings' ); return $settings->has( 'prefix' ) ? (string) $settings->get( 'prefix' ) : 'WC-'; }, - 'api.endpoint.order' => static function ( ContainerInterface $container ): OrderEndpoint { - $order_factory = $container->get( 'api.factory.order' ); - $patch_collection_factory = $container->get( 'api.factory.patch-collection-factory' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); - /** - * The session handler. - * - * @var SessionHandler $session_handler - */ - $session_handler = $container->get( 'session.handler' ); - $bn_code = $session_handler->bn_code(); - - /** - * The settings. - * - * @var Settings $settings - */ - $settings = $container->get( 'wcgateway.settings' ); - $intent = $settings->has( 'intent' ) && strtoupper( (string) $settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE'; - $application_context_repository = $container->get( 'api.repository.application-context' ); - $subscription_helper = $container->get( 'subscription.helper' ); - return new OrderEndpoint( - $container->get( 'api.host' ), - $container->get( 'api.bearer' ), - $order_factory, - $patch_collection_factory, - $intent, - $logger, - $application_context_repository, - $subscription_helper, - $container->get( 'wcgateway.is-fraudnet-enabled' ), - $container->get( 'wcgateway.fraudnet' ), - $bn_code - ); - }, 'woocommerce.logger.woocommerce' => function ( ContainerInterface $container ): LoggerInterface { if ( ! function_exists( 'wc_get_logger' ) || ! $container->get( 'wcgateway.logging.is-enabled' ) ) { return new NullLogger(); From 70d9cfbea0c3983006e480a01b7c40ed758413dc Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 17 Mar 2023 10:23:34 +0200 Subject: [PATCH 06/23] Add API for retrieving paypal order --- api/order-functions.php | 26 ++++++++++ tests/PHPUnit/Api/GetOrderTest.php | 83 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 tests/PHPUnit/Api/GetOrderTest.php diff --git a/api/order-functions.php b/api/order-functions.php index cb15478de..9a8ee4ecd 100644 --- a/api/order-functions.php +++ b/api/order-functions.php @@ -15,10 +15,36 @@ use Exception; use InvalidArgumentException; use RuntimeException; use WC_Order; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\PPCP; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +/** + * Returns the PayPal order. + * + * @param string|WC_Order $paypal_id_or_wc_order The ID of PayPal order or a WC order (with the ID in meta). + * @throws InvalidArgumentException When the argument cannot be used for retrieving the order. + * @throws Exception When the operation fails. + */ +function ppcp_get_paypal_order( $paypal_id_or_wc_order ): Order { + if ( $paypal_id_or_wc_order instanceof WC_Order ) { + $paypal_id_or_wc_order = $paypal_id_or_wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); + if ( ! $paypal_id_or_wc_order ) { + throw new InvalidArgumentException( 'PayPal order ID not found in meta.' ); + } + } + if ( ! is_string( $paypal_id_or_wc_order ) ) { + throw new InvalidArgumentException( 'Invalid PayPal order ID, string expected.' ); + } + + $order_endpoint = PPCP::container()->get( 'api.endpoint.order' ); + assert( $order_endpoint instanceof OrderEndpoint ); + + return $order_endpoint->order( $paypal_id_or_wc_order ); +} + /** * Captures the PayPal order. * diff --git a/tests/PHPUnit/Api/GetOrderTest.php b/tests/PHPUnit/Api/GetOrderTest.php new file mode 100644 index 000000000..362276249 --- /dev/null +++ b/tests/PHPUnit/Api/GetOrderTest.php @@ -0,0 +1,83 @@ +orderEndpoint = Mockery::mock(OrderEndpoint::class); + + $this->bootstrapModule([ + 'api.endpoint.order' => function () { + return $this->orderEndpoint; + }, + ]); + } + + public function testSuccess(): void { + $this->orderEndpoint + ->expects('order') + ->with('123abc') + ->andReturn(Mockery::mock(Order::class)) + ->once(); + + ppcp_get_paypal_order('123abc'); + } + + public function testSuccessWithOrder(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn('123abc'); + + $this->orderEndpoint + ->expects('order') + ->with('123abc') + ->andReturn(Mockery::mock(Order::class)) + ->once(); + + ppcp_get_paypal_order($wcOrder); + } + + public function testOrderWithoutId(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn(false); + + $this->expectException(InvalidArgumentException::class); + + ppcp_get_paypal_order($wcOrder); + } + + public function testFailure(): void { + $this->orderEndpoint + ->expects('order') + ->with('123abc') + ->andThrow(new RuntimeException()) + ->once(); + + $this->expectException(RuntimeException::class); + + ppcp_get_paypal_order('123abc'); + } + + public function testInvalidId(): void { + $this->expectException(InvalidArgumentException::class); + + ppcp_get_paypal_order(123); + } +} From 5dce62fa78346c221eec37dad8e0b022412cca4e Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 17 Mar 2023 11:44:28 +0200 Subject: [PATCH 07/23] Add API for refunding and voiding orders --- api/order-functions.php | 35 +++++ .../src/Processor/RefundProcessor.php | 130 ++++++++++++------ tests/PHPUnit/Api/OrderRefundTest.php | 88 ++++++++++++ tests/PHPUnit/Api/OrderVoidTest.php | 88 ++++++++++++ 4 files changed, 300 insertions(+), 41 deletions(-) create mode 100644 tests/PHPUnit/Api/OrderRefundTest.php create mode 100644 tests/PHPUnit/Api/OrderVoidTest.php diff --git a/api/order-functions.php b/api/order-functions.php index 9a8ee4ecd..8f9f0648a 100644 --- a/api/order-functions.php +++ b/api/order-functions.php @@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\PPCP; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; /** * Returns the PayPal order. @@ -70,3 +71,37 @@ function ppcp_capture_order( WC_Order $wc_order ): void { throw new RuntimeException( 'Capture failed.' ); } } + +/** + * Captures the PayPal order. + * + * @param WC_Order $wc_order The WC order. + * @param float $amount The refund amount. + * @param string $reason The reason for the refund. + * @throws InvalidArgumentException When the order cannot be refunded. + * @throws Exception When the operation fails. + */ +function ppcp_refund_order( WC_Order $wc_order, float $amount, string $reason = '' ): void { + $order = ppcp_get_paypal_order( $wc_order ); + + $refund_processor = PPCP::container()->get( 'wcgateway.processor.refunds' ); + assert( $refund_processor instanceof RefundProcessor ); + + $refund_processor->refund( $order, $wc_order, $amount, $reason ); +} + +/** + * Voids the authorization. + * + * @param WC_Order $wc_order The WC order. + * @throws InvalidArgumentException When the order cannot be voided. + * @throws Exception When the operation fails. + */ +function ppcp_void_order( WC_Order $wc_order ): void { + $order = ppcp_get_paypal_order( $wc_order ); + + $refund_processor = PPCP::container()->get( 'wcgateway.processor.refunds' ); + assert( $refund_processor instanceof RefundProcessor ); + + $refund_processor->void( $order ); +} diff --git a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php index 16f9b4698..cafea380a 100644 --- a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php @@ -11,12 +11,14 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor; use Exception; use Psr\Log\LoggerInterface; +use WC_Order; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -70,7 +72,7 @@ class RefundProcessor { /** * Processes a refund. * - * @param \WC_Order $wc_order The WooCommerce order. + * @param WC_Order $wc_order The WooCommerce order. * @param float|null $amount The refund amount. * @param string $reason The reason for the refund. * @@ -78,7 +80,7 @@ class RefundProcessor { * * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing */ - public function process( \WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool { + public function process( WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool { try { $order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); if ( ! $order_id ) { @@ -87,15 +89,7 @@ class RefundProcessor { $order = $this->order_endpoint->order( $order_id ); - $purchase_units = $order->purchase_units(); - if ( ! $purchase_units ) { - throw new RuntimeException( 'No purchase units.' ); - } - - $payments = $purchase_units[0]->payments(); - if ( ! $payments ) { - throw new RuntimeException( 'No payments.' ); - } + $payments = $this->get_payments( $order ); $this->logger->debug( sprintf( @@ -109,39 +103,11 @@ class RefundProcessor { switch ( $mode ) { case self::REFUND_MODE_REFUND: - $captures = $payments->captures(); - if ( ! $captures ) { - throw new RuntimeException( 'No capture.' ); - } - - $capture = $captures[0]; - $refund = new Refund( - $capture, - $capture->invoice_id(), - $reason, - new Amount( - new Money( $amount, $wc_order->get_currency() ) - ) - ); - $refund_id = $this->payments_endpoint->refund( $refund ); - - $this->add_refund_to_meta( $wc_order, $refund_id ); + $this->refund( $order, $wc_order, $amount, $reason ); break; case self::REFUND_MODE_VOID: - $voidable_authorizations = array_filter( - $payments->authorizations(), - function ( Authorization $authorization ): bool { - return $authorization->is_voidable(); - } - ); - if ( ! $voidable_authorizations ) { - throw new RuntimeException( 'No voidable authorizations.' ); - } - - foreach ( $voidable_authorizations as $authorization ) { - $this->payments_endpoint->void( $authorization ); - } + $this->void( $order ); $wc_order->set_status( 'refunded' ); $wc_order->save(); @@ -158,6 +124,68 @@ class RefundProcessor { } } + /** + * Adds a refund to the PayPal order. + * + * @param Order $order The PayPal order. + * @param WC_Order $wc_order The WooCommerce order. + * @param float $amount The refund amount. + * @param string $reason The reason for the refund. + * + * @throws RuntimeException When operation fails. + */ + public function refund( + Order $order, + WC_Order $wc_order, + float $amount, + string $reason = '' + ): void { + $payments = $this->get_payments( $order ); + + $captures = $payments->captures(); + if ( ! $captures ) { + throw new RuntimeException( 'No capture.' ); + } + + $capture = $captures[0]; + $refund = new Refund( + $capture, + $capture->invoice_id(), + $reason, + new Amount( + new Money( $amount, $wc_order->get_currency() ) + ) + ); + + $refund_id = $this->payments_endpoint->refund( $refund ); + + $this->add_refund_to_meta( $wc_order, $refund_id ); + } + + /** + * Voids the authorization. + * + * @param Order $order The PayPal order. + * @throws RuntimeException When operation fails. + */ + public function void( Order $order ): void { + $payments = $this->get_payments( $order ); + + $voidable_authorizations = array_filter( + $payments->authorizations(), + function ( Authorization $authorization ): bool { + return $authorization->is_voidable(); + } + ); + if ( ! $voidable_authorizations ) { + throw new RuntimeException( 'No voidable authorizations.' ); + } + + foreach ( $voidable_authorizations as $authorization ) { + $this->payments_endpoint->void( $authorization ); + } + } + /** * Determines the refunding mode. * @@ -181,4 +209,24 @@ class RefundProcessor { return self::REFUND_MODE_UNKNOWN; } + + /** + * Returns the payments object or throws. + * + * @param Order $order The order. + * @throws RuntimeException When payment not available. + */ + protected function get_payments( Order $order ): Payments { + $purchase_units = $order->purchase_units(); + if ( ! $purchase_units ) { + throw new RuntimeException( 'No purchase units.' ); + } + + $payments = $purchase_units[0]->payments(); + if ( ! $payments ) { + throw new RuntimeException( 'No payments.' ); + } + + return $payments; + } } diff --git a/tests/PHPUnit/Api/OrderRefundTest.php b/tests/PHPUnit/Api/OrderRefundTest.php new file mode 100644 index 000000000..53b0e2792 --- /dev/null +++ b/tests/PHPUnit/Api/OrderRefundTest.php @@ -0,0 +1,88 @@ +refundProcessor = Mockery::mock(RefundProcessor::class); + $this->orderEndpoint = Mockery::mock(OrderEndpoint::class); + + $this->bootstrapModule([ + 'wcgateway.processor.refunds' => function () { + return $this->refundProcessor; + }, + 'api.endpoint.order' => function () { + return $this->orderEndpoint; + }, + ]); + } + + public function testSuccess(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn('123abc'); + + $this->orderEndpoint + ->expects('order') + ->with('123abc') + ->andReturn(Mockery::mock(Order::class)) + ->once(); + + $this->refundProcessor + ->expects('refund') + ->once(); + + ppcp_refund_order($wcOrder, 42.0, 'reason'); + } + + public function testOrderWithoutId(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn(false); + + $this->expectException(InvalidArgumentException::class); + + ppcp_refund_order($wcOrder, 42.0, 'reason'); + } + + public function testFailure(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn('123abc'); + + $this->orderEndpoint + ->expects('order') + ->with('123abc') + ->andReturn(Mockery::mock(Order::class)) + ->once(); + + $this->refundProcessor + ->expects('refund') + ->andThrow(new RuntimeException()) + ->once(); + + $this->expectException(RuntimeException::class); + + ppcp_refund_order($wcOrder, 42.0, 'reason'); + } +} diff --git a/tests/PHPUnit/Api/OrderVoidTest.php b/tests/PHPUnit/Api/OrderVoidTest.php new file mode 100644 index 000000000..5bbae93a6 --- /dev/null +++ b/tests/PHPUnit/Api/OrderVoidTest.php @@ -0,0 +1,88 @@ +refundProcessor = Mockery::mock(RefundProcessor::class); + $this->orderEndpoint = Mockery::mock(OrderEndpoint::class); + + $this->bootstrapModule([ + 'wcgateway.processor.refunds' => function () { + return $this->refundProcessor; + }, + 'api.endpoint.order' => function () { + return $this->orderEndpoint; + }, + ]); + } + + public function testSuccess(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn('123abc'); + + $this->orderEndpoint + ->expects('order') + ->with('123abc') + ->andReturn(Mockery::mock(Order::class)) + ->once(); + + $this->refundProcessor + ->expects('void') + ->once(); + + ppcp_void_order($wcOrder); + } + + public function testOrderWithoutId(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn(false); + + $this->expectException(InvalidArgumentException::class); + + ppcp_void_order($wcOrder); + } + + public function testFailure(): void { + $wcOrder = Mockery::mock(WC_Order::class); + $wcOrder->expects('get_meta') + ->with(PayPalGateway::ORDER_ID_META_KEY) + ->andReturn('123abc'); + + $this->orderEndpoint + ->expects('order') + ->with('123abc') + ->andReturn(Mockery::mock(Order::class)) + ->once(); + + $this->refundProcessor + ->expects('void') + ->andThrow(new RuntimeException()) + ->once(); + + $this->expectException(RuntimeException::class); + + ppcp_void_order($wcOrder); + } +} From 3b1407aab4a38dea3a9eb99baa488b3de4dd8afc Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 17 Mar 2023 13:01:01 +0200 Subject: [PATCH 08/23] Fix phpdoc --- api/order-functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/order-functions.php b/api/order-functions.php index 8f9f0648a..c0851e77a 100644 --- a/api/order-functions.php +++ b/api/order-functions.php @@ -73,7 +73,7 @@ function ppcp_capture_order( WC_Order $wc_order ): void { } /** - * Captures the PayPal order. + * Refunds the order. * * @param WC_Order $wc_order The WC order. * @param float $amount The refund amount. From 7a994f8d28c6e1778bd572bc81b34f4d01442045 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 17 Mar 2023 17:23:33 +0200 Subject: [PATCH 09/23] Do not save refund id after ppcp_refund_order --- api/order-functions.php | 8 +++++--- .../ppcp-wc-gateway/src/Processor/RefundProcessor.php | 11 ++++++----- tests/PHPUnit/Api/OrderRefundTest.php | 4 +++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/api/order-functions.php b/api/order-functions.php index c0851e77a..f477651fd 100644 --- a/api/order-functions.php +++ b/api/order-functions.php @@ -73,21 +73,23 @@ function ppcp_capture_order( WC_Order $wc_order ): void { } /** - * Refunds the order. + * Refunds the PayPal order. + * Note that you can use wc_refund_payment() to trigger the refund in WC and PayPal. * * @param WC_Order $wc_order The WC order. * @param float $amount The refund amount. * @param string $reason The reason for the refund. + * @return string The PayPal refund ID. * @throws InvalidArgumentException When the order cannot be refunded. * @throws Exception When the operation fails. */ -function ppcp_refund_order( WC_Order $wc_order, float $amount, string $reason = '' ): void { +function ppcp_refund_order( WC_Order $wc_order, float $amount, string $reason = '' ): string { $order = ppcp_get_paypal_order( $wc_order ); $refund_processor = PPCP::container()->get( 'wcgateway.processor.refunds' ); assert( $refund_processor instanceof RefundProcessor ); - $refund_processor->refund( $order, $wc_order, $amount, $reason ); + return $refund_processor->refund( $order, $wc_order, $amount, $reason ); } /** diff --git a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php index cafea380a..0cfea30ad 100644 --- a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php @@ -103,7 +103,9 @@ class RefundProcessor { switch ( $mode ) { case self::REFUND_MODE_REFUND: - $this->refund( $order, $wc_order, $amount, $reason ); + $refund_id = $this->refund( $order, $wc_order, $amount, $reason ); + + $this->add_refund_to_meta( $wc_order, $refund_id ); break; case self::REFUND_MODE_VOID: @@ -133,13 +135,14 @@ class RefundProcessor { * @param string $reason The reason for the refund. * * @throws RuntimeException When operation fails. + * @return string The PayPal refund ID. */ public function refund( Order $order, WC_Order $wc_order, float $amount, string $reason = '' - ): void { + ): string { $payments = $this->get_payments( $order ); $captures = $payments->captures(); @@ -157,9 +160,7 @@ class RefundProcessor { ) ); - $refund_id = $this->payments_endpoint->refund( $refund ); - - $this->add_refund_to_meta( $wc_order, $refund_id ); + return $this->payments_endpoint->refund( $refund ); } /** diff --git a/tests/PHPUnit/Api/OrderRefundTest.php b/tests/PHPUnit/Api/OrderRefundTest.php index 53b0e2792..aa3d4fc4e 100644 --- a/tests/PHPUnit/Api/OrderRefundTest.php +++ b/tests/PHPUnit/Api/OrderRefundTest.php @@ -48,9 +48,11 @@ class OrderRefundTest extends ModularTestCase $this->refundProcessor ->expects('refund') + ->andReturn('456qwe') ->once(); - ppcp_refund_order($wcOrder, 42.0, 'reason'); + $refund_id = ppcp_refund_order($wcOrder, 42.0, 'reason'); + $this->assertEquals('456qwe', $refund_id); } public function testOrderWithoutId(): void { From 84d15f82e5755cabae0171f8933b16c0342b77b8 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 20 Mar 2023 12:07:53 +0100 Subject: [PATCH 10/23] Bump 2.0.4 version --- changelog.txt | 9 +++++++++ package.json | 2 +- readme.txt | 11 ++++++++++- woocommerce-paypal-payments.php | 6 +++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7e19ac016..304dc1b39 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,14 @@ *** Changelog *** += 2.0.4 - TBD = +* Fix - Allow Pay Later in mini-cart #1221 +* Fix - Duplicated auth error when credentials become wrong #1229 +* Fix - Webhook issues when switching sandbox, and delete all webhooks when unsubscribing #1239 +* Enhancement - Remove shortcodes from description #1226 +* Enhancement - Handle price suffix with price for product button check #1234 +* Enhancement - Show funding source as payment method #1220 +* Enhancement - Change "Enabled" to "Available" in status text #1237 + = 2.0.3 - 2023-03-14 = * Fix - `DEVICE_DATA_NOT_AVAILABLE` error message when FraudNet is enabled #1177 * Fix - Redirect to connection tab after manual credentials input #1201 diff --git a/package.json b/package.json index 8a8ec687f..e1370e4e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-paypal-payments", - "version": "2.0.3", + "version": "2.0.4", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", diff --git a/readme.txt b/readme.txt index 9c2e5df68..0c5021a3d 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell, Requires at least: 5.3 Tested up to: 6.1 Requires PHP: 7.2 -Stable tag: 2.0.3 +Stable tag: 2.0.4 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -81,6 +81,15 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == += 2.0.4 - TBD = +* Fix - Allow Pay Later in mini-cart #1221 +* Fix - Duplicated auth error when credentials become wrong #1229 +* Fix - Webhook issues when switching sandbox, and delete all webhooks when unsubscribing #1239 +* Enhancement - Remove shortcodes from description #1226 +* Enhancement - Handle price suffix with price for product button check #1234 +* Enhancement - Show funding source as payment method #1220 +* Enhancement - Change "Enabled" to "Available" in status text #1237 + = 2.0.3 - 2023-03-14 = * Fix - `DEVICE_DATA_NOT_AVAILABLE` error message when FraudNet is enabled #1177 * Fix - Redirect to connection tab after manual credentials input #1201 diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 52766dccf..7f6998d27 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,13 +3,13 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 2.0.3 + * Version: 2.0.4 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.2 * WC requires at least: 3.9 - * WC tested up to: 7.4 + * WC tested up to: 7.5 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; define( 'PAYPAL_API_URL', 'https://api.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2023-02-21' ); +define( 'PAYPAL_INTEGRATION_DATE', '2023-03-20' ); define( 'PPCP_FLAG_SUBSCRIPTION', true ); From 8a1d38ec0ef2f1c77f8502a46ff4fbacf43eb8af Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 21 Mar 2023 11:00:47 +0100 Subject: [PATCH 11/23] Move settings sections service to sections render class --- modules/ppcp-wc-gateway/services.php | 53 +------- .../src/Settings/SectionsRenderer.php | 115 ++++++++++++++++-- 2 files changed, 109 insertions(+), 59 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 7e3ff7eca..6a95c98a2 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -231,8 +231,11 @@ return array( 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { return new SectionsRenderer( $container->get( 'wcgateway.current-ppcp-settings-page-id' ), - $container->get( 'wcgateway.settings.sections' ), - $container->get( 'onboarding.state' ) + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.helper.dcc-product-status' ), + $container->get( 'api.helpers.dccapplies' ), + $container->get( 'button.helper.messages-apply' ), + $container->get( 'wcgateway.pay-upon-invoice-product-status' ) ); }, 'wcgateway.settings.header-renderer' => static function ( ContainerInterface $container ): HeaderRenderer { @@ -241,52 +244,6 @@ return array( $container->get( 'wcgateway.url' ) ); }, - 'wcgateway.settings.sections' => static function ( ContainerInterface $container ): array { - $sections = array( - Settings::CONNECTION_TAB_ID => __( 'Connection', 'woocommerce-paypal-payments' ), - PayPalGateway::ID => __( 'Standard Payments', 'woocommerce-paypal-payments' ), - Settings::PAY_LATER_TAB_ID => __( 'Pay Later', 'woocommerce-paypal-payments' ), - CreditCardGateway::ID => __( 'Advanced Card Processing', 'woocommerce-paypal-payments' ), - CardButtonGateway::ID => __( 'Standard Card Button', 'woocommerce-paypal-payments' ), - OXXOGateway::ID => __( 'OXXO', 'woocommerce-paypal-payments' ), - PayUponInvoiceGateway::ID => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), - ); - - // Remove for all not registered in WC gateways that cannot render anything in this case. - $gateways = WC()->payment_gateways->payment_gateways(); - foreach ( array_diff( - array_keys( $sections ), - array( Settings::CONNECTION_TAB_ID, PayPalGateway::ID, CreditCardGateway::ID, Settings::PAY_LATER_TAB_ID ) - ) as $id ) { - if ( ! isset( $gateways[ $id ] ) ) { - unset( $sections[ $id ] ); - } - } - - $dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' ); - assert( $dcc_product_status instanceof DCCProductStatus ); - $dcc_applies = $container->get( 'api.helpers.dccapplies' ); - assert( $dcc_applies instanceof DccApplies ); - if ( ! $dcc_product_status->dcc_is_active() || ! $dcc_applies->for_country_currency() ) { - unset( $sections['ppcp-credit-card-gateway'] ); - } - - $messages_apply = $container->get( 'button.helper.messages-apply' ); - assert( $messages_apply instanceof MessagesApply ); - - if ( ! $messages_apply->for_country() ) { - unset( $sections[ Settings::PAY_LATER_TAB_ID ] ); - } - - $pui_product_status = $container->get( 'wcgateway.pay-upon-invoice-product-status' ); - assert( $pui_product_status instanceof PayUponInvoiceProductStatus ); - - if ( ! $pui_product_status->pui_is_active() ) { - unset( $sections[ PayUponInvoiceGateway::ID ] ); - } - - return $sections; - }, 'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus { $settings = $container->get( 'wcgateway.settings' ); return new SettingsStatus( $settings ); diff --git a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php index 753caef1a..93ec4a6d4 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php @@ -9,8 +9,16 @@ declare( strict_types=1 ); namespace WooCommerce\PayPalCommerce\WcGateway\Settings; +use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; +use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply; use WooCommerce\PayPalCommerce\Onboarding\State; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; +use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus; +use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; /** * Class SectionsRenderer @@ -26,13 +34,6 @@ class SectionsRenderer { */ protected $page_id; - /** - * Key - page/gateway ID, value - displayed text. - * - * @var array - */ - protected $sections; - /** * The onboarding state. * @@ -40,17 +41,65 @@ class SectionsRenderer { */ private $state; + /** + * The DCC product status + * + * @var DCCProductStatus + */ + private $dcc_product_status; + + /** + * The DCC applies + * + * @var DccApplies + */ + private $dcc_applies; + + /** + * The messages apply. + * + * @var MessagesApply + */ + private $messages_apply; + + /** + * The PUI product status. + * + * @var PayUponInvoiceProductStatus + */ + private $pui_product_status; + /** * SectionsRenderer constructor. * * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. - * @param array $sections Key - page/gateway ID, value - displayed text. * @param State $state The onboarding state. */ - public function __construct( string $page_id, array $sections, State $state ) { + + /** + * SectionsRenderer constructor. + * + * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. + * @param State $state The onboarding state. + * @param DCCProductStatus $dcc_product_status The DCC product status. + * @param DccApplies $dcc_applies The DCC applies. + * @param MessagesApply $messages_apply The Messages apply + * @param PayUponInvoiceProductStatus $pui_product_status The PUI product status. + */ + public function __construct( + string $page_id, + State $state, + DCCProductStatus $dcc_product_status, + DccApplies $dcc_applies, + MessagesApply $messages_apply, + PayUponInvoiceProductStatus $pui_product_status + ) { $this->page_id = $page_id; - $this->sections = $sections; $this->state = $state; + $this->dcc_product_status = $dcc_product_status; + $this->dcc_applies = $dcc_applies; + $this->messages_apply = $messages_apply; + $this->pui_product_status = $pui_product_status; } /** @@ -66,6 +115,8 @@ class SectionsRenderer { /** * Renders the Sections tab. + * + * @return string */ public function render(): string { if ( ! $this->should_render() ) { @@ -74,7 +125,7 @@ class SectionsRenderer { $html = '