From 6410de02040cc67af44f954ecf451023204014ee Mon Sep 17 00:00:00 2001 From: Taha Paksu <3295+tpaksu@users.noreply.github.com> Date: Mon, 21 Jun 2021 22:54:14 +0300 Subject: [PATCH 01/46] Add Jest snap files to editorconfig (#2281) --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index bda37923161..da11c9e8092 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,7 +20,7 @@ trim_trailing_whitespace = true [*.txt] trim_trailing_whitespace = false -[*.{md,json,yml}] +[*.{md,json,yml,snap}] trim_trailing_whitespace = false indent_style = space indent_size = 2 From 710135464b15008225816373caef350b73cb361f Mon Sep 17 00:00:00 2001 From: Bec Scott Date: Tue, 22 Jun 2021 12:18:33 +1000 Subject: [PATCH 02/46] Temporarily don't show the payments notification badge at all (#2291) * Temporarily don't show the payments notification badge at all * Disable unit tests --- includes/admin/class-wc-payments-admin.php | 9 ++++++++- tests/unit/admin/test-class-wc-payments-admin.php | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 154b164a224..8abc3d177a4 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -224,7 +224,14 @@ public function add_payments_menu() { WC_Payments::get_file_version( 'assets/css/admin.css' ) ); - $this->add_menu_notification_badge(); + // Temporarily don't show the badge at all. This should be removed + // when implementing https://github.com/automattic/woocommerce-payments/issues/2071. + // There are also unit tests that need to be re-enabled in + // tests/unit/admin/test-class-wc-payments-admin.php. + if ( true === false ) { + $this->add_menu_notification_badge(); + } + $this->add_update_business_details_task(); } diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index ba7edec9ea1..671d79a1490 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -88,6 +88,13 @@ public function test_it_does_not_render_settings_badge( $is_upe_settings_preview public function test_it_renders_payments_badge_if_stripe_is_not_connected() { global $menu; + // The badge is temporarily not shown at all. See + // class-wc-payments-admin.php line 227. This should be removed when + // implementing https://github.com/automattic/woocommerce-payments/issues/2071. + if ( true === true ) { + return; + } + $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. @@ -102,6 +109,13 @@ public function test_it_renders_payments_badge_if_stripe_is_not_connected() { public function test_it_does_not_render_payments_badge_if_stripe_is_connected() { global $menu; + // The badge is temporarily not shown at all. See + // class-wc-payments-admin.php line 227. This should be removed when + // implementing https://github.com/automattic/woocommerce-payments/issues/2071. + if ( true === true ) { + return; + } + $this->mock_current_user_is_admin(); // Make sure we render the menu with submenu items. From bcd126cd77bcac1569bb72ea91c8f234c2c1ee20 Mon Sep 17 00:00:00 2001 From: Veljko V Date: Tue, 22 Jun 2021 10:33:46 +0200 Subject: [PATCH 03/46] Add new e2e test Systems / Subscriptions / Renew via Action Scheduler (#2260) * Add test structure * Test structure part II * Add evalAndClick to click hidden element * Add calculation * Fix spacing * Finish test * Fix lint js issues --- .../merchant-renew-action-scheduler.spec.js | 99 +++++++++++++++++++ tests/e2e/utils/flows.js | 7 ++ 2 files changed, 106 insertions(+) create mode 100644 tests/e2e/specs/subscriptions/merchant-renew-action-scheduler.spec.js diff --git a/tests/e2e/specs/subscriptions/merchant-renew-action-scheduler.spec.js b/tests/e2e/specs/subscriptions/merchant-renew-action-scheduler.spec.js new file mode 100644 index 00000000000..912a2c8e223 --- /dev/null +++ b/tests/e2e/specs/subscriptions/merchant-renew-action-scheduler.spec.js @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import config from 'config'; + +const { + merchant, + shopper, + withRestApi, + evalAndClick, +} = require( '@woocommerce/e2e-utils' ); + +import { + RUN_SUBSCRIPTIONS_TESTS, + RUN_ACTION_SCHEDULER_TESTS, + describeif, + merchantWCP, +} from '../../utils'; + +import { fillCardDetails, setupCheckout } from '../../utils/payments'; + +const productName = 'Subscription for systems renewal'; +const productSlug = 'subscription-for-systems-renewal'; +const actionSchedulerHook = 'woocommerce_scheduled_subscription_payment'; +const customerBilling = config.get( 'addresses.customer.billing' ); + +describeif( RUN_SUBSCRIPTIONS_TESTS, RUN_ACTION_SCHEDULER_TESTS )( + 'Subscriptions > Renew a subscription via Action Scheduler', + () => { + beforeAll( async () => { + await merchant.login(); + + // Create subscription product with signup fee + await merchantWCP.createSubscriptionProduct( productName, true ); + + await merchant.logout(); + + // Open the subscription product we created in the store + await page.goto( config.get( 'url' ) + `product/${ productSlug }`, { + waitUntil: 'networkidle0', + } ); + + // Add it to the cart and proceed to check out + await expect( page ).toClick( '.single_add_to_cart_button' ); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + await setupCheckout( customerBilling ); + const card = config.get( 'cards.basic' ); + await fillCardDetails( page, card ); + await shopper.placeOrder(); + await expect( page ).toMatch( 'Order received' ); + + await merchant.login(); + } ); + + afterAll( async () => { + // Delete the user created with the subscription + await withRestApi.deleteCustomerByEmail( customerBilling.email ); + await merchant.logout(); + } ); + + it( 'should be able to renew a subscription via Action Scheduler', async () => { + // Go to Action Scheduler + await merchantWCP.openActionScheduler(); + + // Filter results by pending + await page.click( 'ul.subsubsub > .pending > a' ); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + + // Search by pending subscriptions + await expect( page ).toFill( + 'input#plugin-search-input', + actionSchedulerHook + ); + await expect( page ).toClick( 'input#search-submit.button' ); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + + // Run the Action Scheduler task to renew a subscription + await evalAndClick( 'div.row-actions > span.run > a' ); + await page.waitForNavigation( { waitUntil: 'networkidle0' } ); + await expect( page ).toMatchElement( + 'div#message.updated > p > strong', + { + text: actionSchedulerHook, + } + ); + } ); + + it( 'should verify that the subscription has been renewed', async () => { + // Go to Subscriptions and verify the subscription renewal + await merchantWCP.openSubscriptions(); + await expect( + page + ).toMatchElement( + 'tbody#the-list > tr > td.orders.column-orders > a', + { text: '2' } + ); + } ); + } +); diff --git a/tests/e2e/utils/flows.js b/tests/e2e/utils/flows.js index 39ef13fda09..9499792c283 100644 --- a/tests/e2e/utils/flows.js +++ b/tests/e2e/utils/flows.js @@ -30,6 +30,7 @@ const WCPAY_TRANSACTIONS = baseUrl + 'wp-admin/admin.php?page=wc-admin&path=/payments/transactions'; const WC_SUBSCRIPTIONS_PAGE = baseUrl + 'wp-admin/edit.php?post_type=shop_subscription'; +const ACTION_SCHEDULER = baseUrl + 'wp-admin/tools.php?page=action-scheduler'; export const RUN_SUBSCRIPTIONS_TESTS = '1' !== process.env.SKIP_WC_SUBSCRIPTIONS_TESTS; @@ -192,4 +193,10 @@ export const merchantWCP = { } ); await uiLoaded(); }, + + openActionScheduler: async () => { + await page.goto( ACTION_SCHEDULER, { + waitUntil: 'networkidle0', + } ); + }, }; From e8ee926469a55abdf39e31aad566e8b22bd2574a Mon Sep 17 00:00:00 2001 From: Naman Malhotra Date: Tue, 22 Jun 2021 19:26:57 +0530 Subject: [PATCH 04/46] Fix - The "Finish Setup" button just reloads the setup page and doesn't redirect to Stripe onboarding. (#2294) * Fix - Finish Setup button just reloads the setup page during onboarding * Fix failing tests * review changes - rename url to connectUrl * review changes - rename url to connectUrl --- client/connect-account-page/index.js | 10 ++++------ client/connect-account-page/test/index.js | 4 ++-- includes/admin/class-wc-payments-admin.php | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/client/connect-account-page/index.js b/client/connect-account-page/index.js index c06a2114ea0..a72b605e201 100644 --- a/client/connect-account-page/index.js +++ b/client/connect-account-page/index.js @@ -98,10 +98,9 @@ const ConnectPageOnboardingDisabled = () => ( const ConnectPageOnboarding = () => { const [ isSubmitted, setSubmitted ] = useState( false ); const { - availableCountries, - country, - url: connectUrl, - } = wcpaySettings.connect; + connectUrl, + connect: { availableCountries, country }, + } = wcpaySettings; const handleLocationCheck = () => { // Reset the 'Set up' button state if merchant decided to stop @@ -110,8 +109,7 @@ const ConnectPageOnboarding = () => { }; // Redirect the merchant if merchant decided to continue const handleModalConfirmed = () => { - // The raw URL value has ampersands escaped and we need to unescape them for redirect to happen - window.location = connectUrl.replaceAll( '&', '&' ); + window.location = connectUrl; }; // Populate translated list of supported countries we want to render in the modal window. diff --git a/client/connect-account-page/test/index.js b/client/connect-account-page/test/index.js index fedb1a2a6dc..ae9b60762d4 100644 --- a/client/connect-account-page/test/index.js +++ b/client/connect-account-page/test/index.js @@ -14,8 +14,8 @@ describe( 'ConnectAccountPage', () => { beforeEach( () => { window.location.assign = jest.fn(); global.wcpaySettings = { + connectUrl: '/wcpay-connect-url', connect: { - url: '/wcpay-connect-url', country: 'US', availableCountries: { US: 'United States (US)' }, }, @@ -35,8 +35,8 @@ describe( 'ConnectAccountPage', () => { test( 'should prompt unsupported countries', () => { global.wcpaySettings = { + connectUrl: '/wcpay-connect-url', connect: { - url: '/wcpay-connect-url', country: 'CA', availableCountries: { GB: 'United Kingdom (UK)', diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 8abc3d177a4..16c9ce5c727 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -258,8 +258,8 @@ public function register_payments_scripts() { delete_transient( WC_Payments_Account::ERROR_MESSAGE_TRANSIENT ); $wcpay_settings = [ + 'connectUrl' => WC_Payments_Account::get_connect_url(), 'connect' => [ - 'url' => WC_Payments_Account::get_connect_url(), 'country' => WC()->countries->get_base_country(), 'availableCountries' => WC_Payments_Utils::supported_countries(), ], From dc0344dcb34c2a99915dcaca9111ce8fd81ebab0 Mon Sep 17 00:00:00 2001 From: Daniel Mallory Date: Tue, 22 Jun 2021 15:23:46 +0100 Subject: [PATCH 05/46] Client Side fetching and caching of currency rates (#2042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initialize WCPay multi currency, set mock currencies (#1795) * Initialize WCPay multi currency, set mock currencies * Namespaced the classes, fix docblocks, assigned and protected class properties, mock currency handling update. * Change protected properties to public, fix filenames (#1801) * Change protected properties to public, fix dev default currencies, rename files to match classes. * Refactor init of currency object. * Added getters for code and rate * Create data store for multi shopper currency feature (#1856) * Create data store for multi-currecy feature. * Fix copy/paste errors, set hook and selector return names/types correctly. * Basic session management based on url param (#1866) * Fix undefined default currency notices in multi currency tests (#1874) Define a default WooCommerce currency in the tests bootstrap.php using the woocommerce_currency filter to prevent notices caused by not having a default currency set in the test environment. * Added api endpoints for multi shopper currency project. (#1858) * Added api endpoints for multi shopper currency project. * Updated currencies to be returned via a single endpoint. Updated data returned to include necessary information for display. * Fix conflict and lint error from conflict. * Implement JsonSerializable for returning serialized data for Currency object. * Emoji country flags for Multi-Shopper Currencies (#1852) * Initial country flags class to return emojis * Add some tests * Add multi currency tests for Currency and Multi_Currency session handling (#1876) * Add WCPayMultiCurrency test suite * Add tests for WCPay\Multi_Currency\Currency * Add tests for WCPay\Multi_Currency\Multi_Currency session handling * Make Multi_Currency constructor private to prevent it from being instantiated elsewhere * Add flag code to fix Currency serialization test * Move Customer multi-currency tests to tests/unit/multi-currency (#1886) Having tests under the includes folder means that they will be included in the plugin's release zip. This commit moves the tests from includes to the /tests/unit/multi-currency folder and adjusts the tests config file to keep the separate test suite. * Add a customer multi-currency feature flag (#1887) Add a feature flag under the _wcpay_feature_customer_multi_currency option to enable the customer multi-currency code. * Add legacy widget for multi shopper currencies (#1853) * WIP initial legacy widget implementation * Make it functional with a form, and show selected currency Added a form and submit on select change with `this.form.submit()`. * Add aria label to select for better accesibility * Remove parse args from update causing override of selected options * Move widget include to Multi_Currency init * Use DI to pass `Multi_Currency` to widget class And move the widget registration to `Multi_Currency` `init` * Add some widget tests and some minor code improvements * Fix typo * Add missing `$instance` and `id_base` to widget_title filter * Improve instance update booleans readability * Rewrite and split output tests to use expectOutputRegex * Add customer facing prices for customer multi-currency (#1860) * Add type hint to get_default_currency * Add Frontend_Prices to calculate prices for the frontend Frontend_Prices currently supports simple and variation products, shipping methods prices and taxes, and coupon amounts and min_amounts. Also, it shouldn't be loaded on admin pages, modifying the prices only for shoppers. * Add support for setting that controls when charm should be applied * Check if WC session exists before using it Now that get_selected_currency is being used in the frontend, it's possible that some request (e.g. an API request) doesn't have a session set up. This commit checks if a session exists before calling get/set to prevent fatal errors. * Use ceil to the specified precision when rounding prices * Add currency formatting to Frontend_Currencies This commit uses the locale data from WooCommerce core (/i18n/locale-info.php) and extracts the currency settings from it. It also adds the required methods to properly format a currency in the frontend, dealing with the number of decimals, decimal and thousand separators, and price formatting. * Register frontend hooks with a priority higher than the default * Calculate coupon prices without adjustments Coupon prices do not work well with adjustments and could cause issues when rounding up by conceding more discount than intended by the merchants. E.g. if the converted amount is 1 and rounding is set to the tens, that amount would be ceiled to 10 and the customer would get 10x the intended discount. * Run register_free_shipping_filters on init to prevent fatal We cannot call WC_Shipping_Zones::get_zones() on plugin_loaded if the store uses region-specific shipping zones, otherwise, WC throws a fatal. This commit adds a filter to init, so register_free_shipping_filters is called at an appropriate time. * fix for git rebase conflict * Add customer default currency to account settings (#1910) * User settings default currency select implementation * Add some tests for both render and save * Rename `default_currency` to `presentment_currency` to avoid any future confusion * Add the store's currency to the list of available currencies (#1917) The store currency is the currency in which merchants specify all prices. This means that its exchange rate should be 1.0. E.g. the conversion from USD to USD should use a rate of 1 since they're the same currency. Because of that, we can prevent the "Undefined index" error that happens when the store currency is not part of the "available_currencies" array by manually injecting it there. * Save selected customer currency in user settings (#1992) * Fix presentment currency switch typo * Update get_selected_currency to use user meta when logged in * Update update_selected_currency to use user meta when logged in * Add getters for the Frontend Currencies and Prices instances (#2012) We're going to need to fetch some data from the conversion classes when previewing the currency conversion in the settings pages. This commit adds the getters for them in the Multi Currency class, along with changing it so they're always instantiated. The filters are still only registered with frontend requests. * Set customer session cookie manually after select currency (#2020) * Set customer session cookie manually after select currency * Check if session is already created to avoid errors * Check if headers are already sent to avoid tests errors * Recalculate cart when changing currency (#2001) When a currency is selected, the cart on the frontend header is not recalculated. This PR adds a call to force its recalculation so the currency change is reflected and adds the exchange rate and currency code to the cart hash to refresh the cart when the total is 0. * Initial start of settings for multi-currency (#1762) * Rebase settings branch to feature branch. * Create data store for multi-currecy feature. * Fix copy/paste errors, set hook and selector return names/types correctly. * Rebased to feature branch. * Fix rebasing error. * Basic functionality for customer multi currency settings. * Initial start of settings for multi-currency. Items added, but not saving, styled, or dynamic. * Initial start of settings for multi-currency. Items added, but not saving, styled, or dynamic. * Fix merge conflicts. * Fix merge errors after rebase * Fix merge errors after rebase * Minor text changes. * Fix failing test for multi currency json encode * Fix enabled currencies list so that it reloads on update. (#1956) * Fix styling for enabled currencies components, cleaned up classnames. (#1929) * Updated currency symbols to display correctly for enabled currencies. (#1930) * Fix styling for enabled currencies components, cleaned up classnames. * Updated currency symbols to display correctly for enabled currencies. * Better solution for displaying currency symbols. * Better solution for displaying currency symbols. * Fix lint errors after conflict error fix. * Set manual exchange rate field to hide if automatic rate is chosen. (#1977) * Set manual exchange rate field to hide if automatic rate is chosen. * Removed onload event and switched to use forEach to iterate through radio options. * Properly set manual rate if currency uses manual rate. (#1986) * Search functionality for customer multi currencies available modal (#1971) * WIP initial implementation of enabled currencies search * Create a basic search component That uses WC style plus a search icon, and forward all props to the input element * Use the new search component and minor refactor * Move symbol entity decode from JS to PHP on jsonSerialize To allow search by symbol * Style search & footer to include a full width separator * Fix extra whitespace * Remove TODO * Add instance of Multi_Currency class directly into Settings, define parameters before assignment. (#2007) * Separate initialization and getting of currency arrays. (#2006) * Separate initialization and getting of currency arrays. * Separate initialization and getting of currency arrays. * Set default currency to null if it's not found in available currencies. * Removed store settings and preview option in single currency, these will be addressed soon. * Fix errors and failing tests after conflict merge. Co-authored-by: Ismael Martín Alabarce * Fix empty cart display for logged-in users (#2024) Fix empty cart display for logged-in users by moving the add_currency_to_cart_hash hook to run outside of frontend requests. * Add exchange rate to enabled currencies list (#1994) This PR adds an "Exchange rate" header and the exchange rate to the enabled currencies list. Based on the designs, the currency exchange rate was rounded to 2 decimal places. Tests for this change will be added as part of the tests Multi-Currency tests in #2026. * Update multi currency init (#2025) * Move Multi_Currency ID to the class' properties * Add includes function to load the core files * Remove static from init_rest_api * Init widgets using a named function * Reorder is_frontend_request definition * Init settings pages using a named function * Add and inject Multi_Currency as a dependency of Settings * Initial commit, set up functionality to fetch and cache rates. * Initial commit, set up functionality to fetch and cache rates. * Updated the way the API client is passed into the Multi Currency service, linked it up with the API * WIP commit of unit tests * Updates to the unit tests. Still need to add a few more tests for the new functionality. * Unit tests in progress for new functionality * remove call to get_stored_currencies (replaced by get_cached_currencies) * Updated unit tests * renaming variable * code cleanup * Unit test updates * fix for length of caching bug * fix for unsupported PHPUnit test on server * code cleanup * Update the return type of functions in the Multi_Currency class * Update the return type of functions in the Multi_Currency class * Update to the way we init the Multi_Currency plugin * removing change from another PR accidentally added in last commit * moving the order of initialisation * Update to the logic based on changes to server PR * Moving the calls to widget_init and rest_api_init to the consutructor * making the default currency rate 1.0 if none is set * fixed unit tests Co-authored-by: Jesse Pearson Co-authored-by: Jesse Pearson Co-authored-by: Ismael Martín Alabarce Co-authored-by: Luiz Reis --- includes/class-wc-payments.php | 10 +- .../multi-currency/class-multi-currency.php | 244 +++++++++++++----- .../class-wc-payments-api-client.php | 23 ++ .../test-class-multi-currency.php | 194 +++++++++++--- .../test-class-wc-payments-api-client.php | 46 ++++ 5 files changed, 414 insertions(+), 103 deletions(-) diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 2eb92d78eff..02df034f585 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -742,6 +742,15 @@ public static function get_account_service() { return self::$account; } + /** + * Returns the WC_Payments_API_Client + * + * @return WC_Payments_API_Client API Client instance + */ + public static function get_payments_api_client() { + return self::$api_client; + } + /** * Registers the payment method with the blocks registry. * @@ -815,5 +824,4 @@ public static function remove_woo_admin_notes() { public static function is_network_saved_cards_enabled() { return apply_filters( 'wcpay_force_network_saved_cards', false ); } - } diff --git a/includes/multi-currency/class-multi-currency.php b/includes/multi-currency/class-multi-currency.php index 0199274c28d..d644378c0b8 100644 --- a/includes/multi-currency/class-multi-currency.php +++ b/includes/multi-currency/class-multi-currency.php @@ -7,6 +7,10 @@ namespace WCPay\Multi_Currency; +use WC_Payments; +use WC_Payments_API_Client; +use WCPay\Exceptions\API_Exception; + defined( 'ABSPATH' ) || exit; /** @@ -14,8 +18,10 @@ */ class Multi_Currency { - const CURRENCY_SESSION_KEY = 'wcpay_currency'; - const CURRENCY_META_KEY = 'wcpay_currency'; + const CURRENCY_SESSION_KEY = 'wcpay_currency'; + const CURRENCY_META_KEY = 'wcpay_currency'; + const CURRENCY_CACHE_OPTION = 'wcpay_multi_currency_cached_currencies'; + const CURRENCY_RETRIEVAL_ERROR = 'error'; /** * The plugin's ID. @@ -66,6 +72,13 @@ class Multi_Currency { */ protected $enabled_currencies; + /** + * Client for making requests to the WooCommerce Payments API + * + * @var WC_Payments_API_Client + */ + private $payments_api_client; + /** * Main Multi_Currency Instance. * @@ -76,41 +89,48 @@ class Multi_Currency { */ public static function instance() { if ( is_null( self::$instance ) ) { - self::$instance = new self(); + self::$instance = new self( WC_Payments::get_payments_api_client() ); } return self::$instance; } /** - * Constructor. + * Class constructor. + * + * @param WC_Payments_API_Client $payments_api_client Payments API client. */ - private function __construct() { + public function __construct( WC_Payments_API_Client $payments_api_client ) { + // Load the include files. $this->includes(); - $this->init(); + + $this->payments_api_client = $payments_api_client; + + add_action( 'init', [ $this, 'init' ] ); + add_action( 'rest_api_init', [ $this, 'init_rest_api' ] ); + add_action( 'widgets_init', [ $this, 'init_widgets' ] ); + + $is_frontend_request = ! is_admin() && ! defined( 'DOING_CRON' ) && ! WC()->is_rest_api_request(); + + if ( $is_frontend_request ) { + // Make sure that this runs after the main init function. + add_action( 'init', [ $this, 'update_selected_currency_by_url' ], 11 ); + } } /** - * Init. + * Called after the WooCommerce session has been initialized. Initialises the available currencies, + * default currency and enabled currencies for the multi currency plugin. */ public function init() { $this->initialize_available_currencies(); $this->set_default_currency(); $this->initialize_enabled_currencies(); - add_action( 'rest_api_init', [ $this, 'init_rest_api' ] ); - add_action( 'widgets_init', [ $this, 'init_widgets' ] ); - new User_Settings( $this ); $this->frontend_prices = new Frontend_Prices( $this ); $this->frontend_currencies = new Frontend_Currencies( $this ); - $is_frontend_request = ! is_admin() && ! defined( 'DOING_CRON' ) && ! WC()->is_rest_api_request(); - - if ( $is_frontend_request ) { - add_action( 'init', [ $this, 'update_selected_currency_by_url' ] ); - } - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); if ( is_admin() ) { @@ -143,7 +163,7 @@ public function init_widgets() { * * @return array The new settings pages. */ - public function init_settings_pages( $settings_pages ) { + public function init_settings_pages( $settings_pages ): array { include_once WCPAY_ABSPATH . 'includes/multi-currency/class-settings.php'; $settings_pages[] = new Settings( $this ); @@ -188,21 +208,59 @@ public function enqueue_scripts() { } /** - * Gets the mock available. + * Wipes the cached currency data option, forcing to re-fetch the data from WPCOM. * - * @return array Array of currencies. + * @return void */ - public function get_mock_currencies() { + public function clear_cache() { + delete_option( self::CURRENCY_CACHE_OPTION ); + } + + /** + * Gets and caches the data for the currency rates from the server. + * Will be returned as an array with three keys, 'currencies' (the currencies), 'expires' (the expiry time) + * and 'updated' (when this data was fetched from the API). + * + * @return ?array + */ + public function get_cached_currencies() { + if ( ! $this->payments_api_client->is_server_connected() ) { + return null; + } + + $cache_data = $this->read_currencies_from_cache(); + + // If the option contains the error value, return false early and do not attempt another API call. + if ( isset( $cache_data['currencies'] ) && self::CURRENCY_RETRIEVAL_ERROR === $cache_data['currencies'] ) { + return null; + } + + // If an array of currencies was returned from the cache, return it here. + if ( null !== $cache_data ) { + return $cache_data; + } + + // If the cache was expired or something went wrong, make a call to the server to get the + // currency data. + try { + $currency_data = $this->payments_api_client->get_currency_rates( get_woocommerce_currency() ); + } catch ( API_Exception $e ) { + // Failed to retrieve currencies from the server. Exception is logged in http client. + // Rate limit for a short amount of time by caching the failure. + $this->cache_currencies( self::CURRENCY_RETRIEVAL_ERROR, time(), 1 * MINUTE_IN_SECONDS ); + + // Return null to signal currency retrieval error. + return null; + } + + $updated = time(); + + // Cache the currency data so we don't call the server every time. + $this->cache_currencies( $currency_data, $updated, 6 * HOUR_IN_SECONDS ); + return [ - [ 'CAD', 1.206823 ], - [ 'GBP', 0.708099 ], - [ 'EUR', 0.826381 ], - [ 'AED', 3.6732 ], - [ 'CDF', 2000 ], - [ 'NZD', 1.387163 ], - [ 'DKK', 6.144615 ], - [ 'BIF', 1974 ], // Zero decimal currency. - [ 'CLP', 706.8 ], // Zero decimal currency. + 'currencies' => $currency_data, + 'updated' => $updated, ]; } @@ -211,7 +269,7 @@ public function get_mock_currencies() { * * @return Frontend_Prices */ - public function get_frontend_prices() { + public function get_frontend_prices(): Frontend_Prices { return $this->frontend_prices; } @@ -220,23 +278,10 @@ public function get_frontend_prices() { * * @return Frontend_Currencies */ - public function get_frontend_currencies() { + public function get_frontend_currencies(): Frontend_Currencies { return $this->frontend_currencies; } - /** - * Gets the currencies stored in the db. - * - * @return array Multi-dimensional array of currencies and rates. - */ - private function get_stored_currencies(): array { - $stored_currencies = get_option( $this->id . '_stored_currencies', false ); - if ( ! $stored_currencies ) { - $stored_currencies = $this->get_mock_currencies(); - } - return $stored_currencies; - } - /** * Sets up the available currencies, which are alphabetical by name. */ @@ -245,9 +290,16 @@ private function initialize_available_currencies() { $woocommerce_currency = get_woocommerce_currency(); $this->available_currencies[ $woocommerce_currency ] = new Currency( $woocommerce_currency, 1.0 ); - $currencies = $this->get_stored_currencies(); - foreach ( $currencies as $currency ) { - $new_currency = new Currency( $currency[0], $currency[1] ); + $available_currencies = []; + + $wc_currencies = get_woocommerce_currencies(); + $cache_data = $this->get_cached_currencies(); + + foreach ( $wc_currencies as $currency_code => $currency_name ) { + $currency_rate = $cache_data['currencies'][ $currency_code ] ?? 1.0; + $new_currency = new Currency( $currency_code, $currency_rate ); + + // Add this to our list of available currencies. $available_currencies[ $new_currency->get_name() ] = $new_currency; } @@ -369,6 +421,7 @@ public function get_selected_currency(): Currency { * Update the selected currency from a currency code. * * @param string $currency_code Three letter currency code. + * * @return void */ public function update_selected_currency( string $currency_code ) { @@ -411,17 +464,10 @@ public function update_selected_currency_by_url() { $this->update_selected_currency( sanitize_text_field( wp_unslash( $_GET['currency'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification } - /** - * Recalculates WooCommerce cart totals. - */ - public function recalculate_cart() { - WC()->cart->calculate_totals(); - } - /** * Gets the configured value for apply charm pricing only to products. * - * @return bool The configured value. + * @return mixed The configured value. */ public function get_apply_charm_only_to_products() { return apply_filters( 'wcpay_multi_currency_apply_charm_only_to_products', true ); @@ -457,6 +503,33 @@ public function get_price( $price, $type ): float { return $this->get_adjusted_price( $converted_price, $apply_charm_pricing, $currency ); } + /** + * Recalculates WooCommerce cart totals. + */ + public function recalculate_cart() { + WC()->cart->calculate_totals(); + } + + /** + * Adds Multi-Currency notes to the WC-Admin inbox. + */ + public static function add_woo_admin_notes() { + if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { + require_once WCPAY_ABSPATH . 'includes/multi-currency/notes/class-note-multi-currency-available.php'; + Note_Multi_Currency_Available::possibly_add_note(); + } + } + + /** + * Removes Multi-Currency notes from the WC-Admin inbox. + */ + public static function remove_woo_admin_notes() { + if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { + require_once WCPAY_ABSPATH . 'includes/multi-currency/notes/class-note-multi-currency-available.php'; + Note_Multi_Currency_Available::possibly_delete_note(); + } + } + /** * Gets the price after adjusting it with the rounding and charm settings. * @@ -487,7 +560,7 @@ protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ) * * @return float The ceiled price. */ - protected function ceil_price( $price, $precision ) { + protected function ceil_price( $price, $precision ): float { $precision_modifier = pow( 10, $precision ); return ceil( $price * $precision_modifier ) / $precision_modifier; } @@ -505,22 +578,61 @@ protected function includes() { } /** - * Adds Multi-Currency notes to the WC-Admin inbox. + * Caches currency data for a period of time. + * + * @param string|array $currencies - Currency data to cache. + * @param int|null $updated - The time the data was fetched from the server. + * @param int|null $expiration - The length of time to cache the currency data, in seconds. + * + * @return bool */ - public static function add_woo_admin_notes() { - if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { - require_once WCPAY_ABSPATH . 'includes/multi-currency/notes/class-note-multi-currency-available.php'; - Note_Multi_Currency_Available::possibly_add_note(); + private function cache_currencies( $currencies, int $updated = null, int $expiration = null ): bool { + // Default $expiration to 6 hours if not set. + if ( null === $expiration ) { + $expiration = 6 * HOUR_IN_SECONDS; + } + + // Default $updated to the current time. + if ( null === $updated ) { + $updated = time(); } + + // Add the currency data, expiry time, and time updated to the array we're caching. + $currency_cache = [ + 'currencies' => $currencies, + 'expires' => time() + $expiration, + 'updated' => $updated, + ]; + + // Create or update the currency option cache. + if ( false === get_option( self::CURRENCY_CACHE_OPTION ) ) { + $result = add_option( self::CURRENCY_CACHE_OPTION, $currency_cache, '', 'no' ); + } else { + $result = update_option( self::CURRENCY_CACHE_OPTION, $currency_cache, 'no' ); + } + + return $result; } /** - * Removes Multi-Currency notes from the WC-Admin inbox. + * Read the currency data from the WP option we cache it in. + * + * @return ?array */ - public static function remove_woo_admin_notes() { - if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.4.0', '>=' ) ) { - require_once WCPAY_ABSPATH . 'includes/multi-currency/notes/class-note-multi-currency-available.php'; - Note_Multi_Currency_Available::possibly_delete_note(); + private function read_currencies_from_cache() { + $currency_cache = get_option( self::CURRENCY_CACHE_OPTION ); + + if ( false === $currency_cache || ! isset( $currency_cache['currencies'] ) || ! isset( $currency_cache['expires'] ) || ! isset( $currency_cache['updated'] ) ) { + // No option found or the data isn't in the format we expect. + return null; } + + // Return false if the cache has expired, triggering another fetch. + if ( $currency_cache['expires'] < time() ) { + return null; + } + + // We have fresh currency data in the cache, so return it. + return $currency_cache; } } diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php index 5eb3e023fd5..c97c4ca8722 100644 --- a/includes/wc-payment-api/class-wc-payments-api-client.php +++ b/includes/wc-payment-api/class-wc-payments-api-client.php @@ -30,6 +30,7 @@ class WC_Payments_API_Client { const CHARGES_API = 'charges'; const CONN_TOKENS_API = 'terminal/connection_tokens'; const CUSTOMERS_API = 'customers'; + const CURRENCY_API = 'currency'; const INTENTIONS_API = 'intentions'; const REFUNDS_API = 'refunds'; const DEPOSITS_API = 'deposits'; @@ -805,6 +806,28 @@ public function get_timeline( $intention_id ) { return $this->request( [], self::TIMELINE_API . '/' . $intention_id, self::GET ); } + /** + * Get currency rates from the server. + * + * @param string $currency_from - The currency to convert from. + * @param ?array $currencies_to - An array of the currencies we want to convert into. If left empty, will get all supported currencies. + * + * @return array + */ + public function get_currency_rates( string $currency_from, $currencies_to = null ) { + $query_body = [ 'currency_from' => $currency_from ]; + + if ( null !== $currencies_to ) { + $query_body['currencies_to'] = $currencies_to; + } + + return $this->request( + $query_body, + self::CURRENCY_API . '/rates', + self::GET + ); + } + /** * Get current account data * diff --git a/tests/unit/multi-currency/test-class-multi-currency.php b/tests/unit/multi-currency/test-class-multi-currency.php index 59ba395c075..8f51279b339 100644 --- a/tests/unit/multi-currency/test-class-multi-currency.php +++ b/tests/unit/multi-currency/test-class-multi-currency.php @@ -5,33 +5,45 @@ * @package WooCommerce\Payments\Tests */ +use WCPay\Exceptions\API_Exception; +use WCPay\Multi_Currency\Multi_Currency; + /** * WCPay\Multi_Currency\Multi_Currency unit tests. */ class WCPay_Multi_Currency_Tests extends WP_UnitTestCase { - const LOGGED_IN_USER_ID = 1; + const LOGGED_IN_USER_ID = 1; + const ENABLED_CURRENCIES_OPTION = 'wcpay_multi_currency_enabled_currencies'; + const CACHED_CURRENCIES_OPTION = 'wcpay_multi_currency_cached_currencies'; + + /** + * Mock enabled currencies. + * + * @var array + */ + private $mock_enabled_currencies = [ 'USD', 'CAD', 'GBP', 'BIF' ]; /** - * Mock available currencies. + * Mock available currencies with their rates. * * @var array */ - public $mock_available_currencies = [ - [ 'USD', 1 ], - [ 'CAD', 1.206823 ], - [ 'GBP', 0.708099 ], - [ 'EUR', 0.826381 ], - [ 'CDF', 2000 ], - [ 'BIF', 1974 ], // Zero decimal currency. - [ 'CLP', 706.8 ], // Zero decimal currency. + private $mock_available_currencies = [ + 'USD' => 1, + 'CAD' => 1.206823, + 'GBP' => 0.708099, + 'EUR' => 0.826381, + 'CDF' => 2000, + 'BIF' => 1974, // Zero decimal currency. + 'CLP' => 706.8, // Zero decimal currency. ]; /** - * Mock enabled currencies. + * Mock cached currencies return array * * @var array */ - public $mock_enabled_currencies = [ 'USD', 'CAD', 'GBP', 'BIF' ]; + private $mock_cached_currencies; /** * WCPay\Multi_Currency\Multi_Currency instance. @@ -40,6 +52,13 @@ class WCPay_Multi_Currency_Tests extends WP_UnitTestCase { */ private $multi_currency; + /** + * Mock of the API client. + * + * @var WC_Payments_API_Client + */ + private $mock_api_client; + public function setUp() { parent::setUp(); @@ -50,24 +69,41 @@ public function setUp() { 'price_rounding' => '0', ] ); - update_option( 'wcpay_multi_currency_stored_currencies', $this->mock_available_currencies ); - update_option( 'wcpay_multi_currency_enabled_currencies', $this->mock_enabled_currencies ); - $this->multi_currency = WCPay\Multi_Currency\Multi_Currency::instance(); + $this->mock_cached_currencies = [ + 'currencies' => $this->mock_available_currencies, + 'updated' => strtotime( 'today midnight' ), + 'expires' => strtotime( 'today midnight' ) + DAY_IN_SECONDS, + ]; + + update_option( self::CACHED_CURRENCIES_OPTION, $this->mock_cached_currencies ); + update_option( self::ENABLED_CURRENCIES_OPTION, $this->mock_enabled_currencies ); + + $this->mock_api_client = $this->getMockBuilder( WC_Payments_API_Client::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'get_currency_rates', 'is_server_connected' ] ) + ->getMock(); + + $this->mock_api_client + ->expects( $this->any() ) + ->method( 'is_server_connected' ) + ->willReturn( true ); + + $this->multi_currency = new Multi_Currency( $this->mock_api_client ); + $this->multi_currency->init(); } public function tearDown() { - WC()->session->__unset( WCPay\Multi_Currency\Multi_Currency::CURRENCY_SESSION_KEY ); + WC()->session->__unset( Multi_Currency::CURRENCY_SESSION_KEY ); remove_all_filters( 'wcpay_multi_currency_apply_charm_only_to_products' ); remove_all_filters( 'woocommerce_currency' ); - $this->reset_multi_currency_instance(); - delete_user_meta( self::LOGGED_IN_USER_ID, WCPay\Multi_Currency\Multi_Currency::CURRENCY_META_KEY ); + delete_user_meta( self::LOGGED_IN_USER_ID, Multi_Currency::CURRENCY_META_KEY ); wp_set_current_user( 0 ); $this->remove_currency_settings_mock( 'GBP', [ 'price_charm', 'price_rounding' ] ); - delete_option( 'wcpay_multi_currency_stored_currencies' ); - delete_option( 'wcpay_multi_currency_enabled_currencies' ); + delete_option( self::CACHED_CURRENCIES_OPTION ); + delete_option( self::ENABLED_CURRENCIES_OPTION ); parent::tearDown(); } @@ -82,8 +118,8 @@ function () { ); // Recreate Multi_Currency instance to use the recently set DEFAULT currency. - $this->reset_multi_currency_instance(); - $this->multi_currency = WCPay\Multi_Currency\Multi_Currency::instance(); + $this->multi_currency = new Multi_Currency( $this->mock_api_client ); + $this->multi_currency->init(); $default_currency = $this->multi_currency->get_available_currencies()['DEFAULT']; @@ -119,15 +155,15 @@ public function test_get_enabled_currencies_returns_sorted_currencies() { public function test_set_enabled_currencies() { $currencies = [ 'USD', 'EUR', 'GBP', 'CLP' ]; $this->multi_currency->set_enabled_currencies( $currencies ); - $this->assertSame( $currencies, get_option( 'wcpay_multi_currency_enabled_currencies' ) ); + $this->assertSame( $currencies, get_option( self::ENABLED_CURRENCIES_OPTION ) ); } public function test_enabled_but_unavailable_currencies_are_skipped() { - update_option( 'wcpay_multi_currency_enabled_currencies', [ 'RANDOM_CURRENCY', 'USD' ] ); + update_option( self::ENABLED_CURRENCIES_OPTION, [ 'RANDOM_CURRENCY', 'USD' ] ); // Recreate Multi_Currency instance to use the recently set currencies. - $this->reset_multi_currency_instance(); - $this->multi_currency = WCPay\Multi_Currency\Multi_Currency::instance(); + $this->multi_currency = new Multi_Currency( $this->mock_api_client ); + $this->multi_currency->init(); $this->assertSame( [ 'USD' ], array_keys( $this->multi_currency->get_enabled_currencies() ) ); } @@ -292,14 +328,108 @@ public function test_get_price_converts_using_ceil_and_precision( $price, $preci $this->mock_currency_settings( 'GBP', [ 'price_rounding' => $precision ] ); // Recreate Multi_Currency instance to use the recently set price_rounding. - $this->reset_multi_currency_instance(); - $this->multi_currency = WCPay\Multi_Currency\Multi_Currency::instance(); + $this->multi_currency = new Multi_Currency( $this->mock_api_client ); + $this->multi_currency->init(); WC()->session->set( WCPay\Multi_Currency\Multi_Currency::CURRENCY_SESSION_KEY, 'GBP' ); $this->assertSame( $expected, $this->multi_currency->get_price( $price, 'shipping' ) ); } + public function test_get_cached_currencies_with_no_server_connection() { + // Need to create a new instance of Multi_Currency with a different $mock_api_client + // Because the mock return value of 'is_server_connected' cannot be overridden. + $mock_api_client = $this->getMockBuilder( WC_Payments_API_Client::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'get_currency_rates', 'is_server_connected' ] ) + ->getMock(); + + $mock_api_client + ->expects( $this->any() ) + ->method( 'is_server_connected' ) + ->willReturn( false ); + + $this->multi_currency = new Multi_Currency( $mock_api_client ); + $this->multi_currency->init(); + $this->assertNull( $this->multi_currency->get_cached_currencies() ); + } + + public function test_get_cached_currencies_with_server_retrieval_error() { + $current_time = time(); + + $currency_cache = [ + 'currencies' => Multi_Currency::CURRENCY_RETRIEVAL_ERROR, + 'updated' => $current_time, + 'expires' => $current_time + DAY_IN_SECONDS, + ]; + + // Create or update the currency option cache. + update_option( Multi_Currency::CURRENCY_CACHE_OPTION, $currency_cache, 'no' ); + + $this->assertNull( $this->multi_currency->get_cached_currencies() ); + } + + public function test_get_cached_currencies_with_valid_cached_data() { + update_option( self::CACHED_CURRENCIES_OPTION, $this->mock_cached_currencies ); + + $this->assertEquals( + $this->mock_cached_currencies, + $this->multi_currency->get_cached_currencies() + ); + } + + public function test_get_cached_currencies_fetches_from_server() { + delete_option( self::CACHED_CURRENCIES_OPTION ); + + $currency_from = get_woocommerce_currency(); + $currencies_to = get_woocommerce_currencies(); + unset( $currencies_to[ $currency_from ] ); + + $this->mock_api_client + ->expects( $this->once() ) + ->method( 'get_currency_rates' ) + ->with( + $currency_from + )->willReturn( + $this->mock_available_currencies + ); + + $result = $this->multi_currency->get_cached_currencies(); + + // Assert that the currencies and the time updated were returned. + $this->assertArrayHasKey( 'currencies', $result ); + $this->assertArrayHasKey( 'updated', $result ); + $this->assertEquals( + $this->mock_available_currencies, + $result['currencies'] + ); + + // Assert that the cache was correctly set. + $cached_data = get_option( self::CACHED_CURRENCIES_OPTION ); + $this->assertTrue( is_array( $cached_data ) ); + $this->assertArrayHasKey( 'currencies', $cached_data ); + $this->assertArrayHasKey( 'updated', $cached_data ); + $this->assertEquals( + $this->mock_available_currencies, + $result['currencies'] + ); + } + + public function test_get_cached_currencies_handles_api_exception() { + delete_option( self::CACHED_CURRENCIES_OPTION ); + + $this->mock_api_client + ->expects( $this->once() ) + ->method( 'get_currency_rates' ) + ->willThrowException( new API_Exception( 'Error connecting to server', 'API_ERROR', 500 ) ); + + $this->assertNull( $this->multi_currency->get_cached_currencies() ); + + // Assert that the cache was correctly set with the error string. + $cached_data = get_option( self::CACHED_CURRENCIES_OPTION ); + $this->assertEquals( Multi_Currency::CURRENCY_RETRIEVAL_ERROR, $cached_data['currencies'] ); + } + public function get_price_provider() { return [ [ '7.07', '2', 5.01 ], // 5.006 after conversion @@ -318,14 +448,6 @@ public function get_price_provider() { ]; } - private function reset_multi_currency_instance() { - $multi_currency_reflection = new ReflectionClass( $this->multi_currency ); - $instance_property = $multi_currency_reflection->getProperty( 'instance' ); - $instance_property->setAccessible( true ); - $instance_property->setValue( null, null ); - $instance_property->setAccessible( false ); - } - private function mock_currency_settings( $currency_code, $settings ) { foreach ( $settings as $setting => $value ) { update_option( 'wcpay_multi_currency_' . $setting . '_' . strtolower( $currency_code ), $value ); diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php index d4a86dde8a9..765870672a5 100644 --- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php +++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php @@ -763,6 +763,52 @@ public function test_add_tos_agreement() { $this->assertEquals( [ 'result' => 'success' ], $result ); } + public function test_get_currency_rates() { + $currency_from = 'USD'; + + $this->mock_http_client + ->expects( $this->once() ) + ->method( 'remote_request' ) + ->with( + [ + 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/currency/rates?test_mode=0¤cy_from=USD', + 'method' => 'GET', + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'User-Agent' => 'Unit Test Agent/0.1.0', + ], + 'timeout' => 70, + 'connect_timeout' => 70, + ], + null, + true, + false + )->willReturn( + [ + 'body' => wp_json_encode( + [ + 'GBP' => 0.75, + 'EUR' => 0.82, + ] + ), + 'response' => [ + 'code' => 200, + 'message' => 'OK', + ], + ] + ); + + $result = $this->payments_api_client->get_currency_rates( $currency_from ); + + $this->assertEquals( + [ + 'GBP' => 0.75, + 'EUR' => 0.82, + ], + $result + ); + } + /** * @dataProvider data_request_with_level3_data */ From 0b9f113a0ce2b971a4c7458eded714f41caf0ee3 Mon Sep 17 00:00:00 2001 From: Luiz Reis Date: Tue, 22 Jun 2021 11:58:20 -0300 Subject: [PATCH 06/46] Only output single currency breadcrumbs when rendering the settings page (#2284) * Only output single currency breadcrumbs when rendering the settings page The 'save' method calls 'get_settings', causing the breadcrumbs to be rendered also when submitting the single currency settings form. This commit adds a check to prevent rendering the breadcrumbs when not rendering the settings page, avoiding the warning and extra breadcrumbs at the top of the page. * Move single currency breadcrumbs to the woocommerce_settings_ hook We should avoid outputting in get_settings, as it's also used to get the settings in non-rendering contexts. This commit moves the breadcrumbs logic to the woocommerce_settings_wcpay_multi_currency hook and renders them when viewing a single currency page. --- includes/multi-currency/class-settings.php | 32 +++++-- .../multi-currency/test-class-settings.php | 86 +++++++++++++++++++ 2 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 tests/unit/multi-currency/test-class-settings.php diff --git a/includes/multi-currency/class-settings.php b/includes/multi-currency/class-settings.php index 38e9087af0d..ea844991a1b 100644 --- a/includes/multi-currency/class-settings.php +++ b/includes/multi-currency/class-settings.php @@ -52,6 +52,8 @@ public function __construct( Multi_Currency $multi_currency ) { add_action( 'woocommerce_admin_field_wcpay_currencies_settings_section_end', [ $this, 'currencies_settings_section_end' ] ); add_action( 'woocommerce_admin_field_wcpay_single_currency_preview_helper', [ $this, 'single_currency_preview_helper' ] ); + + add_action( 'woocommerce_settings_' . $this->id, [ $this, 'render_single_currency_breadcrumbs' ] ); parent::__construct(); } @@ -310,13 +312,6 @@ public function get_currency_setting( $currency ) { $currency->get_code() ); - // Output breadcrumbs. - ?> -

- > get_name()} ({$currency->get_code()}) {$currency->get_flag()}" ); ?> -

- id . '_single_settings', [ @@ -426,4 +421,27 @@ public function save() { do_action( 'woocommerce_update_options_' . $this->id ); } + + /** + * Renders the breadcrumbs for the single currency settings page. + */ + public function render_single_currency_breadcrumbs() { + global $current_section; + + $currency_code = strtoupper( $current_section ); + $currencies = $this->multi_currency->get_enabled_currencies(); + + // Exit early if this is not a single currency page or the currency is not enabled. + if ( empty( $current_section ) || ! isset( $currencies[ $currency_code ] ) ) { + return; + } + + $currency = $currencies[ $currency_code ]; + + ?> +

+ > get_name()} ({$currency->get_code()}) {$currency->get_flag()}" ); ?> +

+ mock_multi_currency = $this->createMock( WCPay\Multi_Currency\Multi_Currency::class ); + + // The settings pages file is only included in woocommerce_get_settings_pages, so we need to manually include it here. + include_once WCPAY_ABSPATH . 'includes/multi-currency/class-settings.php'; + $this->settings = new WCPay\Multi_Currency\Settings( $this->mock_multi_currency ); + } + + /** + * @dataProvider woocommerce_action_provider + */ + public function test_registers_woocommerce_action( $action, $function_name ) { + $this->assertNotFalse( + has_action( $action, [ $this->settings, $function_name ] ), + "Action '$action' was not registered with '$function_name'" + ); + } + + public function woocommerce_action_provider() { + return [ + [ 'woocommerce_settings_wcpay_multi_currency', 'render_single_currency_breadcrumbs' ], + [ 'woocommerce_admin_field_wcpay_enabled_currencies_list', 'enabled_currencies_list' ], + [ 'woocommerce_admin_field_wcpay_currencies_settings_section_start', 'currencies_settings_section_start' ], + [ 'woocommerce_admin_field_wcpay_currencies_settings_section_end', 'currencies_settings_section_end' ], + [ 'woocommerce_admin_field_wcpay_single_currency_preview_helper', 'single_currency_preview_helper' ], + ]; + } + + public function test_render_single_currency_breadcrumbs_does_not_render_when_blank_section() { + $GLOBALS['current_section'] = ''; + + $this->expectOutputString( '' ); + + $this->settings->render_single_currency_breadcrumbs(); + } + + public function test_render_single_currency_breadcrumbs_does_not_render_when_currency_not_enabled() { + $GLOBALS['current_section'] = 'gbp'; + $this->mock_multi_currency->method( 'get_enabled_currencies' )->willReturn( [] ); + + $this->expectOutputString( '' ); + + $this->settings->render_single_currency_breadcrumbs(); + } + + public function test_render_single_currency_breadcrumbs_renders_breadcrumbs_for_single_currency() { + $GLOBALS['current_section'] = 'gbp'; + $this->mock_multi_currency->method( 'get_enabled_currencies' )->willReturn( [ 'GBP' => new Currency( 'GBP' ) ] ); + + $this->expectOutputRegex( '/Currencies<\/a> > Pound sterling \(GBP\) 🇬🇧/' ); + + $this->settings->render_single_currency_breadcrumbs(); + } +} From 3cd978bf850c8f1de33b0dd53b0ad67948f742b3 Mon Sep 17 00:00:00 2001 From: Francesco Date: Tue, 22 Jun 2021 14:20:39 -0500 Subject: [PATCH 07/46] fix: DigitalWallets settings click through (#2300) --- client/settings/digital-wallets/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/settings/digital-wallets/style.scss b/client/settings/digital-wallets/style.scss index c6f1aa5a354..b8503bb5643 100644 --- a/client/settings/digital-wallets/style.scss +++ b/client/settings/digital-wallets/style.scss @@ -15,7 +15,7 @@ &.is-enabled { &::after { - opacity: 0; + display: none; } } } From 06ad35edec56d92e87d6d2f9a22bf39b81ffc403 Mon Sep 17 00:00:00 2001 From: Francesco Date: Tue, 22 Jun 2021 16:06:29 -0500 Subject: [PATCH 08/46] fix: all payment methods icons column display (#2302) --- includes/class-wc-payment-gateway-wcpay.php | 63 +++++++++++++++---- .../class-cc-payment-gateway.php | 43 ------------- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 00f6ff6c778..5312d14a61b 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -292,6 +292,16 @@ public function __construct( // no longer valid options. unset( $this->form_fields['payment_request_button_type']['options']['branded'] ); unset( $this->form_fields['payment_request_button_type']['options']['custom'] ); + + add_filter( + 'woocommerce_payment_gateways_setting_columns', + [ $this, 'add_all_payment_methods_logos_column' ] + ); + + add_action( + 'woocommerce_payment_gateways_setting_column_logos', + [ $this, 'add_all_payment_methods_icon_logos' ] + ); } // Giropay option hidden behind feature flag. @@ -354,21 +364,50 @@ public function __construct( // Update the current request logged_in cookie after a guest user is created to avoid nonce inconsistencies. add_action( 'set_logged_in_cookie', [ $this, 'set_cookie_on_current_request' ] ); + } - /** - * Add a new logo column on the right of "method" in the payment methods table. - */ - add_filter( - 'woocommerce_payment_gateways_setting_columns', - function( $columns ) { - $logos = [ 'logos' => '' ]; // Setting an ID for the column, but not a label. - $offset = array_search( 'name', array_keys( $columns ), true ) + 1; + /** + * Add a new logo column on the right of "method" in the payment methods table. + * + * @param array $columns the columns in the "all payment methods" page. + * @return array + */ + public function add_all_payment_methods_logos_column( $columns ) { + $logos = [ 'logos' => '' ]; // Setting an ID for the column, but not a label. + $offset = array_search( 'name', array_keys( $columns ), true ) + 1; - $columns = array_merge( array_slice( $columns, 0, $offset ), $logos, array_slice( $columns, $offset ) ); + return array_merge( array_slice( $columns, 0, $offset ), $logos, array_slice( $columns, $offset ) ); + } - return $columns; - } - ); + /** + * Add a list of payment method logos to WooCommerce Payment in the logo column. + * + * @param WC_Payment_Gateway $gateway the current gateway iterated over to be displayed in the "all payment methods" page. + */ + public function add_all_payment_methods_icon_logos( $gateway ) { + if ( 'woocommerce_payments' !== $gateway->id ) { + echo ''; + + return; + } + + $icons = [ + 'visa', + 'mastercard', + 'amex', + 'apple-pay', + 'google-pay', + ]; + + echo ''; + ?> +
+ + + +
+ '; } /** diff --git a/includes/payment-methods/class-cc-payment-gateway.php b/includes/payment-methods/class-cc-payment-gateway.php index f6c7493ebc9..aa785b3948a 100644 --- a/includes/payment-methods/class-cc-payment-gateway.php +++ b/includes/payment-methods/class-cc-payment-gateway.php @@ -19,47 +19,4 @@ * Right now behaves exactly like WC_Payment_Gateway_WCPay for max compatibility. */ class CC_Payment_Gateway extends WC_Payment_Gateway_WCPay { - /** - * Constructor same parameters as WC_Payment_Gateway_WCPay constructor. - * - * @param WC_Payments_API_Client $payments_api_client - WooCommerce Payments API client. - * @param WC_Payments_Account $account - Account class instance. - * @param WC_Payments_Customer_Service $customer_service - Customer class instance. - * @param WC_Payments_Token_Service $token_service - Token class instance. - * @param WC_Payments_Action_Scheduler_Service $action_scheduler_service - Action Scheduler service instance. - */ - public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, WC_Payments_Customer_Service $customer_service, WC_Payments_Token_Service $token_service, WC_Payments_Action_Scheduler_Service $action_scheduler_service ) { - parent::__construct( $payments_api_client, $account, $customer_service, $token_service, $action_scheduler_service ); - - /** - * Add a list of payment method logos to WooCommerce Payment in the logo column. - */ - add_action( - 'woocommerce_payment_gateways_setting_column_logos', - function( $gateway ) { - if ( 'woocommerce_payments' !== $gateway->id ) { - echo ''; - return; - } - - $icons = [ - 'visa', - 'mastercard', - 'amex', - 'apple-pay', - 'google-pay', - ]; - - echo ''; - ?> -
- - - -
- '; - } - ); - } } From e7a2f906d3b9cd1740c86f387f22ca92b14cf057 Mon Sep 17 00:00:00 2001 From: Chris Aprea Date: Wed, 23 Jun 2021 09:40:15 +1000 Subject: [PATCH 09/46] Redirect to onboarding when account disconnected (#2275) Co-authored-by: Chris --- changelog.txt | 1 + includes/admin/class-wc-payments-admin.php | 139 +++++++++++------- includes/class-wc-payments-account.php | 2 +- readme.txt | 1 + .../admin/test-class-wc-payments-admin.php | 89 ++++++++++- 5 files changed, 174 insertions(+), 58 deletions(-) diff --git a/changelog.txt b/changelog.txt index 40e91bf4f6b..1604e9eb9dd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -8,6 +8,7 @@ * Fix - Use of deprecated call-style to registerPaymentMethods. WooCommerce Payments now requires WooCommerce Blocks of at least version 3.9.0. * Fix - Deposit date on Transactions list page. * Fix - Rounding error when displaying fee percentages on the Overview and Transactions pages. +* Fix - WooCommerce Payments admin pages redirect to the onboarding page when the WooCommerce Payments account is disconnected. * Add - Error message when total size of dispute evidence files uploaded goes over limit. * Update - Pass currency to wc_price when adding intent notes to orders. * Update - Instant deposit inbox note wording. diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 16c9ce5c727..6e2d5a86b57 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -40,6 +40,13 @@ class WC_Payments_Admin { */ private $account; + /** + * WCPay admin child pages. + * + * @var array + */ + private $admin_child_pages; + /** * Hook in admin menu items. * @@ -58,8 +65,52 @@ public function __construct( // Add menu items. add_action( 'admin_menu', [ $this, 'add_payments_menu' ], 0 ); + add_action( 'admin_menu', [ $this, 'maybe_redirect_to_onboarding' ], 1 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_payments_scripts' ] ); add_action( 'woocommerce_admin_field_payment_gateways', [ $this, 'payment_gateways_container' ] ); + + $this->admin_child_pages = [ + 'wc-payments-overview' => [ + 'id' => 'wc-payments-overview', + 'title' => __( 'Overview', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/overview', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 10, + ], + ], + 'wc-payments-deposits' => [ + 'id' => 'wc-payments-deposits', + 'title' => __( 'Deposits', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/deposits', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 20, + ], + ], + 'wc-payments-transactions' => [ + 'id' => 'wc-payments-transactions', + 'title' => __( 'Transactions', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/transactions', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 30, + ], + ], + 'wc-payments-disputes' => [ + 'id' => 'wc-payments-disputes', + 'title' => __( 'Disputes', 'woocommerce-payments' ), + 'parent' => 'wc-payments', + 'path' => '/payments/disputes', + 'nav_args' => [ + 'parent' => 'wc-payments', + 'order' => 40, + ], + ], + ]; } /** @@ -106,57 +157,9 @@ public function add_payments_menu() { * wc_admin_register_page to duplicate "Payments" menu item as a * first item in the sub-menu. */ - wc_admin_register_page( - [ - 'id' => 'wc-payments-overview', - 'title' => __( 'Overview', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/overview', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 10, - ], - ] - ); - - wc_admin_register_page( - [ - 'id' => 'wc-payments-deposits', - 'title' => __( 'Deposits', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/deposits', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 20, - ], - ] - ); - - wc_admin_register_page( - [ - 'id' => 'wc-payments-transactions', - 'title' => __( 'Transactions', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/transactions', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 30, - ], - ] - ); - - wc_admin_register_page( - [ - 'id' => 'wc-payments-disputes', - 'title' => __( 'Disputes', 'woocommerce-payments' ), - 'parent' => 'wc-payments', - 'path' => '/payments/disputes', - 'nav_args' => [ - 'parent' => 'wc-payments', - 'order' => 40, - ], - ] - ); + foreach ( $this->admin_child_pages as $admin_child_page ) { + wc_admin_register_page( $admin_child_page ); + } wc_admin_connect_page( [ @@ -607,4 +610,38 @@ private function is_in_treatment_mode() { return 'treatment' === $abtest->get_variation( 'wcpay_empty_state_preview_mode' ); } + + /** + * Checks if Stripe account is connected and redirects to the onboarding page + * if it is not and the user is attempting to view a WCPay admin page. + */ + public function maybe_redirect_to_onboarding() { + $url_params = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification + + if ( empty( $url_params['page'] ) || 'wc-admin' !== $url_params['page'] ) { + return; + } + + $current_path = ! empty( $url_params['path'] ) ? $url_params['path'] : ''; + + if ( empty( $current_path ) ) { + return; + } + + $page_paths = []; + + foreach ( $this->admin_child_pages as $payments_child_page ) { + $page_paths[] = preg_quote( $payments_child_page['path'], '/' ); + } + + if ( ! preg_match( '/^(' . implode( '|', $page_paths ) . ')/', $current_path ) ) { + return; + } + + if ( $this->account->is_stripe_connected() ) { + return; + } + + $this->account->redirect_to_onboarding_page(); + } } diff --git a/includes/class-wc-payments-account.php b/includes/class-wc-payments-account.php index a61e663e987..3fcc0601b8c 100644 --- a/includes/class-wc-payments-account.php +++ b/includes/class-wc-payments-account.php @@ -233,7 +233,7 @@ public function get_fraud_services_config() { * * @param string $error_message Optional error message to show in a notice. */ - private function redirect_to_onboarding_page( $error_message = null ) { + public function redirect_to_onboarding_page( $error_message = null ) { if ( isset( $error_message ) ) { set_transient( self::ERROR_MESSAGE_TRANSIENT, $error_message, 30 ); } diff --git a/readme.txt b/readme.txt index a295e79125b..29f49d28b54 100644 --- a/readme.txt +++ b/readme.txt @@ -108,6 +108,7 @@ Please note that our support for the checkout block is still experimental and th * Fix - Use of deprecated call-style to registerPaymentMethods. WooCommerce Payments now requires WooCommerce Blocks of at least version 3.9.0. * Fix - Deposit date on Transactions list page. * Fix - Rounding error when displaying fee percentages on the Overview and Transactions pages. +* Fix - WooCommerce Payments admin pages redirect to the onboarding page when the WooCommerce Payments account is disconnected. * Add - Error message when total size of dispute evidence files uploaded goes over limit. * Update - Pass currency to wc_price when adding intent notes to orders. * Update - Instant deposit inbox note wording. diff --git a/tests/unit/admin/test-class-wc-payments-admin.php b/tests/unit/admin/test-class-wc-payments-admin.php index 671d79a1490..fb307f3142b 100644 --- a/tests/unit/admin/test-class-wc-payments-admin.php +++ b/tests/unit/admin/test-class-wc-payments-admin.php @@ -43,6 +43,11 @@ public function setUp() { $this->payments_admin = new WC_Payments_Admin( $mock_api_client, $mock_gateway, $this->mock_account ); } + public function tearDown() { + unset( $_GET ); + parent::tearDown(); + } + public function test_it_renders_settings_badge_if_upe_settings_preview_is_enabled_and_upe_is_not() { global $submenu; @@ -91,9 +96,7 @@ public function test_it_renders_payments_badge_if_stripe_is_not_connected() { // The badge is temporarily not shown at all. See // class-wc-payments-admin.php line 227. This should be removed when // implementing https://github.com/automattic/woocommerce-payments/issues/2071. - if ( true === true ) { - return; - } + static::markTestSkipped( 'Not yet implemented.' ); $this->mock_current_user_is_admin(); @@ -112,9 +115,7 @@ public function test_it_does_not_render_payments_badge_if_stripe_is_connected() // The badge is temporarily not shown at all. See // class-wc-payments-admin.php line 227. This should be removed when // implementing https://github.com/automattic/woocommerce-payments/issues/2071. - if ( true === true ) { - return; - } + static::markTestSkipped( 'Not yet implemented.' ); $this->mock_current_user_is_admin(); @@ -139,4 +140,80 @@ private function mock_current_user_is_admin() { $admin_user = self::factory()->user->create( [ 'role' => 'administrator' ] ); wp_set_current_user( $admin_user ); } + + /** + * @dataProvider data_maybe_redirect_to_onboarding + */ + public function test_maybe_redirect_to_onboarding( $expected_times_redirect_called, $is_stripe_connected, $get_params ) { + $_GET = $get_params; + + $this->mock_account + ->method( 'is_stripe_connected' ) + ->willReturn( $is_stripe_connected ); + + $this->mock_account + ->expects( $this->exactly( $expected_times_redirect_called ) ) + ->method( 'redirect_to_onboarding_page' ); + + $this->payments_admin->maybe_redirect_to_onboarding(); + } + + /** + * Data provider for test_maybe_redirect_to_onboarding + */ + public function data_maybe_redirect_to_onboarding() { + return [ + 'no_get_params' => [ + 0, + false, + [], + ], + 'empty_page_param' => [ + 0, + false, + [ + 'path' => '/payments/overview', + ], + ], + 'incorrect_page_param' => [ + 0, + false, + [ + 'page' => 'wc-settings', + 'path' => '/payments/overview', + ], + ], + 'empty_path_param' => [ + 0, + false, + [ + 'page' => 'wc-admin', + ], + ], + 'incorrect_path_param' => [ + 0, + false, + [ + 'page' => 'wc-admin', + 'path' => '/payments/does-not-exist', + ], + ], + 'stripe_connected' => [ + 0, + true, + [ + 'page' => 'wc-admin', + 'path' => '/payments/overview', + ], + ], + 'happy_path' => [ + 1, + false, + [ + 'page' => 'wc-admin', + 'path' => '/payments/overview', + ], + ], + ]; + } } From fe71b7fd6ecb324397786ce666da69ba07f69f4c Mon Sep 17 00:00:00 2001 From: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:59:31 +0800 Subject: [PATCH 10/46] Bump Required Versions for 2.6 Release (#2263) * Bump minimum suported WooCommerce version * Add changelog * Fix typo * Bump WC tested up to Co-authored-by: James Rodger --- changelog.txt | 1 + readme.txt | 3 ++- woocommerce-payments.php | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1604e9eb9dd..e287c3c0912 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ * Tweak - Run post-upgrade actions during any request instead of only on wp-admin requests. * Update - Payment request button should guide users to login when necessary. * Add - When setting WooCommerce Payments up, inform if merchant business country is not supported. +* Update - Bump minimum supported version of WooCommerce from 4.8 to 5.2. * Add - Introduce advance filters on deposits page. * Update: Prefill OAuth flow with WC store country diff --git a/readme.txt b/readme.txt index 29f49d28b54..1b32fffcc7b 100644 --- a/readme.txt +++ b/readme.txt @@ -40,7 +40,7 @@ Our global support team is available to answer questions you may have about WooC * United States-based business. * WordPress 5.5 or newer. -* WooCommerce 4.8 or newer. +* WooCommerce 5.2 or newer. * PHP version 7.0 or newer. PHP 7.2 or newer is recommended. = Try it now = @@ -115,6 +115,7 @@ Please note that our support for the checkout block is still experimental and th * Fix - Deposit overview details for non instant ones. * Update - Payment request button should guide users to login when necessary. * Add - When setting WooCommerce Payments up, inform if merchant business country is not supported. +* Update - Bump minimum supported version of WooCommerce from 4.8 to 5.2. * Add - Introduce advance filters on deposits page. * Update: Prefill OAuth flow with WC store country diff --git a/woocommerce-payments.php b/woocommerce-payments.php index 50c1585658d..20372dcfa75 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -9,7 +9,7 @@ * Text Domain: woocommerce-payments * Domain Path: /languages * WC requires at least: 4.0 - * WC tested up to: 5.1 + * WC tested up to: 5.4 * Requires WP: 5.5 * Version: 2.5.0 * From 57fbe732c3a40b9fae8c62420ef540e001882778 Mon Sep 17 00:00:00 2001 From: Luiz Reis Date: Wed, 23 Jun 2021 09:09:39 -0300 Subject: [PATCH 11/46] Update customer multi-currency default settings and options (#2280) * Update the "Charm pricing" label to read "Price charm" * Remove the tooltips for Price rounding and charm * Remove the "Learn more" links from the Price rounding and charm descriptions * Remove the "(recommended)" label from the -0.01 charm option * Fix typo in preview description * Modify get_ceil_price to accept an arbitrary rounding value We were using the decimal place to round to in get_ceil_price, but it doesn't support values such as 0.25 or 0.50. This commit modifies it to round to the next multiple of $rounding, instead of the decimal place to support such values. * Update decimal currency rounding options * Add zero decimal currencies price rounding and charm options * Update default price rounding option --- client/multi-currency/index.js | 3 +- .../multi-currency/class-multi-currency.php | 18 ++-- includes/multi-currency/class-settings.php | 90 +++++++++++-------- .../test-class-multi-currency.php | 64 +++++++------ 4 files changed, 100 insertions(+), 75 deletions(-) diff --git a/client/multi-currency/index.js b/client/multi-currency/index.js index 88a7dc3036f..129d53172fc 100644 --- a/client/multi-currency/index.js +++ b/client/multi-currency/index.js @@ -86,8 +86,7 @@ function updatePreview() { let total = previewAmount.value * rate; if ( 'none' !== rounding.value ) { - const precision = Math.pow( 10, rounding.value ); - total = Math.ceil( total * precision ) / precision; + total = Math.ceil( total / rounding.value ) * rounding.value; } total += parseFloat( charm.value ); diff --git a/includes/multi-currency/class-multi-currency.php b/includes/multi-currency/class-multi-currency.php index d644378c0b8..10baeda753b 100644 --- a/includes/multi-currency/class-multi-currency.php +++ b/includes/multi-currency/class-multi-currency.php @@ -333,7 +333,7 @@ function( $currency ) use ( $enabled_currency_codes ) { // Get the charm and rounding for each enabled currency and add the currencies to the object property. $currency = clone $enabled_currency; $charm = get_option( $this->id . '_price_charm_' . $currency->get_id(), 0.00 ); - $rounding = get_option( $this->id . '_price_rounding_' . $currency->get_id(), 'none' ); + $rounding = get_option( $this->id . '_price_rounding_' . $currency->get_id(), $currency->get_is_zero_decimal() ? '100' : '1.00' ); $currency->set_charm( $charm ); $currency->set_rounding( $rounding ); @@ -541,7 +541,7 @@ public static function remove_woo_admin_notes() { */ protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ): float { if ( 'none' !== $currency->get_rounding() ) { - $price = $this->ceil_price( $price, intval( $currency->get_rounding() ) ); + $price = $this->ceil_price( $price, floatval( $currency->get_rounding() ) ); } if ( $apply_charm_pricing ) { @@ -553,16 +553,18 @@ protected function get_adjusted_price( $price, $apply_charm_pricing, $currency ) } /** - * Ceils the price to the next number based on the precision. + * Ceils the price to the next number based on the rounding value. * - * @param float $price The price to be ceiled. - * @param int $precision The precision to be used. + * @param float $price The price to be ceiled. + * @param float $rounding The rounding option. * * @return float The ceiled price. */ - protected function ceil_price( $price, $precision ): float { - $precision_modifier = pow( 10, $precision ); - return ceil( $price * $precision_modifier ) / $precision_modifier; + protected function ceil_price( float $price, float $rounding ): float { + if ( 0.00 === $rounding ) { + return $price; + } + return ceil( $price / $rounding ) * $rounding; } /** diff --git a/includes/multi-currency/class-settings.php b/includes/multi-currency/class-settings.php index ea844991a1b..580d3557299 100644 --- a/includes/multi-currency/class-settings.php +++ b/includes/multi-currency/class-settings.php @@ -271,43 +271,53 @@ public function get_currency_setting( $currency ) { 'manual' => __( 'Manual rate. Enter your own fixed rate of exchange.', 'woocommerce-payments' ), ]; - $rounding_desc = sprintf( - /* translators: %1$s currency being converted to, %2$s url to documentation. */ - __( 'Make your %1$s prices consistent by rounding them up after they are converted.
Learn more', 'woocommerce-payments' ), - $currency->get_code(), - // TODO: Url to documentation needed. - '' - ); + $decimal_currency_rounding_options = [ + 'none' => __( 'None', 'woocommerce-payments' ), + '0.25' => '0.25', + '0.50' => '0.50', + '1' => '1.00 (recommended)', + '5' => '5.00', + '10' => '10.00', + ]; + $zero_decimal_currency_rounding_options = [ + 'none' => __( 'None', 'woocommerce-payments' ), + '10' => '10', + '25' => '25', + '50' => '50', + '100' => '100 (recommended)', + '500' => '500', + '1000' => '1000', + ]; $rounding_options = apply_filters( $this->id . '_price_rounding_options', - [ - 'none' => __( 'None', 'woocommerce-payments' ), - '-1' => '10.00', - '0' => '1.00 (recommended)', - '1' => '0.10', - ] + $currency->get_is_zero_decimal() ? $zero_decimal_currency_rounding_options : $decimal_currency_rounding_options ); - $charm_desc = sprintf( - /* translators: %s: url to documentation.*/ - __( 'Reduce the converted price by a specific amount. Learn more', 'woocommerce-payments' ), - // TODO: Url to documentation needed. - '' - ); + $decimal_currency_charm_options = [ + '0.00' => __( 'None', 'woocommerce-payments' ), + '-0.01' => '-0.01', + '-0.05' => '-0.05', + ]; + $zero_decimal_currency_charm_options = [ + '0.00' => __( 'None', 'woocommerce-payments' ), + '-1' => '-1', + '-5' => '-5', + '-10' => '-10', + '-20' => '-20', + '-25' => '-25', + '-50' => '-50', + '-100' => '-100', + ]; $charm_options = apply_filters( $this->id . '_charm_options', - [ - '0.00' => __( 'None', 'woocommerce-payments' ), - '-0.01' => __( '-0.01 (recommended)', 'woocommerce-payments' ), - '-0.05' => '-0.05', - ] + $currency->get_is_zero_decimal() ? $zero_decimal_currency_charm_options : $decimal_currency_charm_options ); $preview_desc = sprintf( /* translators: %1$s: default currency of the store, %2$s currency being converted to. */ - __( 'Enter a price in your default currency of %1$s to see it converted into %2$s using the excange rate and formatting rules above.', 'woocommerce-payments' ), + __( 'Enter a price in your default currency of %1$s to see it converted into %2$s using the exchange rate and formatting rules above.', 'woocommerce-payments' ), $default_currency->get_code(), $currency->get_code() ); @@ -354,23 +364,25 @@ public function get_currency_setting( $currency ) { ], [ - 'title' => __( 'Price rounding', 'woocommerce-payments' ), - 'desc' => $rounding_desc, - 'id' => $this->id . '_price_rounding_' . $currency->get_id(), - 'default' => 'none', - 'type' => 'select', - 'options' => $rounding_options, - 'desc_tip' => __( 'Conversion rates at the bank may differ from current conversion rates. Rounding up to the nearest whole number helps prevent losses on sales.', 'woocommerce-payments' ), + 'title' => __( 'Price rounding', 'woocommerce-payments' ), + 'desc' => sprintf( + /* translators: %1$s currency being converted to. */ + __( 'Make your %1$s prices consistent by rounding them up after they are converted.', 'woocommerce-payments' ), + $currency->get_code() + ), + 'id' => $this->id . '_price_rounding_' . $currency->get_id(), + 'default' => $currency->get_is_zero_decimal() ? '100' : '1.00', + 'type' => 'select', + 'options' => $rounding_options, ], [ - 'title' => __( 'Charm pricing', 'woocommerce-payments' ), - 'desc' => $charm_desc, - 'id' => $this->id . '_price_charm_' . $currency->get_id(), - 'default' => '0.00', - 'type' => 'select', - 'options' => $charm_options, - 'desc_tip' => __( 'A value of -0.01 would reduce 20.00 to 19.99.', 'woocommerce-payments' ), + 'title' => __( 'Price charm', 'woocommerce-payments' ), + 'desc' => __( 'Reduce the converted price by a specific amount.', 'woocommerce-payments' ), + 'id' => $this->id . '_price_charm_' . $currency->get_id(), + 'default' => '0.00', + 'type' => 'select', + 'options' => $charm_options, ], [ diff --git a/tests/unit/multi-currency/test-class-multi-currency.php b/tests/unit/multi-currency/test-class-multi-currency.php index 8f51279b339..d614956f06f 100644 --- a/tests/unit/multi-currency/test-class-multi-currency.php +++ b/tests/unit/multi-currency/test-class-multi-currency.php @@ -66,7 +66,7 @@ public function setUp() { 'GBP', [ 'price_charm' => '-0.1', - 'price_rounding' => '0', + 'price_rounding' => '0.50', ] ); @@ -101,7 +101,7 @@ public function tearDown() { delete_user_meta( self::LOGGED_IN_USER_ID, Multi_Currency::CURRENCY_META_KEY ); wp_set_current_user( 0 ); - $this->remove_currency_settings_mock( 'GBP', [ 'price_charm', 'price_rounding' ] ); + $this->remove_currency_settings_mock( 'GBP', [ 'price_charm', 'price_rounding', 'manual_rate', 'exchange_rate' ] ); delete_option( self::CACHED_CURRENCIES_OPTION ); delete_option( self::ENABLED_CURRENCIES_OPTION ); @@ -138,11 +138,13 @@ public function test_get_enabled_currencies_returns_correctly() { foreach ( $mock_currencies as $code => $rate ) { $currency = new WCPay\Multi_Currency\Currency( $code, $rate ); $currency->set_charm( 0.00 ); - $currency->set_rounding( 'none' ); + $currency->set_rounding( '1.00' ); $expected[ $currency->get_code() ] = $currency; } $expected['GBP']->set_charm( '-0.1' ); - $expected['GBP']->set_rounding( '0' ); + $expected['GBP']->set_rounding( '0.50' ); + // Zero-decimal currencies should default to rounding = 100. + $expected['BIF']->set_rounding( '100' ); $this->assertEquals( $expected, $this->multi_currency->get_enabled_currencies() ); } @@ -276,24 +278,24 @@ public function test_get_price_returns_converted_product_price_with_charm() { WC()->session->set( WCPay\Multi_Currency\Multi_Currency::CURRENCY_SESSION_KEY, 'GBP' ); add_filter( 'wcpay_multi_currency_apply_charm_only_to_products', '__return_true' ); - // 0.708099 * 10 = 7,08099 -> ceiled to 8 -> 8 - 0.1 = 7.9 - $this->assertSame( 7.9, $this->multi_currency->get_price( '10.0', 'product' ) ); + // 0.708099 * 10 = 7,08099 -> ceiled to 7.5 -> 7.5 - 0.1 = 7.4 + $this->assertSame( 7.4, $this->multi_currency->get_price( '10.0', 'product' ) ); } public function test_get_price_returns_converted_shipping_price_with_charm() { WC()->session->set( WCPay\Multi_Currency\Multi_Currency::CURRENCY_SESSION_KEY, 'GBP' ); add_filter( 'wcpay_multi_currency_apply_charm_only_to_products', '__return_false' ); - // 0.708099 * 10 = 7,08099 -> ceiled to 8 -> 8 - 0.1 = 7.9 - $this->assertSame( 7.9, $this->multi_currency->get_price( '10.0', 'shipping' ) ); + // 0.708099 * 10 = 7,08099 -> ceiled to 7.5 -> 7.5 - 0.1 = 7.4 + $this->assertSame( 7.4, $this->multi_currency->get_price( '10.0', 'shipping' ) ); } public function test_get_price_returns_converted_shipping_price_without_charm() { WC()->session->set( WCPay\Multi_Currency\Multi_Currency::CURRENCY_SESSION_KEY, 'GBP' ); add_filter( 'wcpay_multi_currency_apply_charm_only_to_products', '__return_true' ); - // 0.708099 * 10 = 7,08099 -> ceiled to 8 -> 8 + 0.0 = 8.0 - $this->assertSame( 8.0, $this->multi_currency->get_price( '10.0', 'shipping' ) ); + // 0.708099 * 10 = 7,08099 -> ceiled to 7.5 -> 7.5 + 0.0 = 7.5 + $this->assertSame( 7.5, $this->multi_currency->get_price( '10.0', 'shipping' ) ); } public function test_get_price_returns_converted_coupon_price_without_adjustments() { @@ -323,9 +325,16 @@ public function test_get_price_returns_converted_tax_price() { /** * @dataProvider get_price_provider */ - public function test_get_price_converts_using_ceil_and_precision( $price, $precision, $expected ) { + public function test_get_price_converts_using_ceil_and_precision( $target_price, $precision, $expected ) { add_filter( 'wcpay_multi_currency_apply_charm_only_to_products', '__return_true' ); - $this->mock_currency_settings( 'GBP', [ 'price_rounding' => $precision ] ); + $this->mock_currency_settings( + 'GBP', + [ + 'price_rounding' => $precision, + 'exchange_rate' => 'manual', + 'manual_rate' => $target_price, + ] + ); // Recreate Multi_Currency instance to use the recently set price_rounding. $this->multi_currency = new Multi_Currency( $this->mock_api_client ); @@ -333,7 +342,7 @@ public function test_get_price_converts_using_ceil_and_precision( $price, $preci WC()->session->set( WCPay\Multi_Currency\Multi_Currency::CURRENCY_SESSION_KEY, 'GBP' ); - $this->assertSame( $expected, $this->multi_currency->get_price( $price, 'shipping' ) ); + $this->assertSame( $expected, $this->multi_currency->get_price( 1, 'shipping' ) ); } public function test_get_cached_currencies_with_no_server_connection() { @@ -432,19 +441,22 @@ public function test_get_cached_currencies_handles_api_exception() { public function get_price_provider() { return [ - [ '7.07', '2', 5.01 ], // 5.006 after conversion - [ '7.06', '2', 5.0 ], // 4.999 after conversion - [ '7.04', '2', 4.99 ], // 4.985 after conversion - [ '7.07', '1', 5.1 ], // 5.006 after conversion - [ '7.06', '1', 5.0 ], // 4.999 after conversion - [ '6.90', '1', 4.9 ], // 4.885 after conversion - [ '7.07', '0', 6.0 ], // 5.006 after conversion - [ '7.06', '0', 5.0 ], // 4.999 after conversion - [ '5.80', '0', 5.0 ], // 4.106 after conversion - [ '14.26', '-1', 20.0 ], // 10.097 after conversion - [ '14.02', '-1', 10.0 ], // 9.927 after conversion - [ '141.0', '-2', 100.0 ], // 99.841 after conversion - [ '142.0', '-2', 200.0 ], // 100.550 after conversion + [ '5.2499', '0.00', 5.2499 ], + [ '5.2499', '0.25', 5.25 ], + [ '5.2500', '0.25', 5.25 ], + [ '5.2501', '0.25', 5.50 ], + [ '5.4999', '0.50', 5.50 ], + [ '5.5000', '0.50', 5.50 ], + [ '5.5001', '0.50', 6.00 ], + [ '4.9999', '1.00', 5.00 ], + [ '5.0000', '1.00', 5.00 ], + [ '5.0001', '1.00', 6.00 ], + [ '4.9999', '5.00', 5.00 ], + [ '5.0000', '5.00', 5.00 ], + [ '5.0001', '5.00', 10.00 ], + [ '9.9999', '10.00', 10.00 ], + [ '10.000', '10.00', 10.00 ], + [ '10.0001', '10.00', 20.00 ], ]; } From 491c390abfd183ec1f56592d4d0622dac231fc92 Mon Sep 17 00:00:00 2001 From: Jesse Pearson Date: Wed, 23 Jun 2021 12:17:57 -0300 Subject: [PATCH 12/46] Fix double conversion issues on manual subscription renewals. (#2165) * Fix double conversion issues on manual subscription renewals. * Fix double conversion issues on manual subscription renewals. * Fix double conversion issues on manual subscription renewals. * Fix to only not convert prices for subscription product renewal in the cart and checkout. * Move compatibility code out to its own class. * Set up backtrace check for compatibility usage. * Pass Compatibily object into constructors of classes that use it. Remove Multi_Currency object from Compatibily class due to it is not used. Fix and add tests. * Add tests for Compatibility class, other small code fixes. * Create Utils class for additional methods, create tests for utils, complete tests for Compatibility. * Fix tests, small refactors for cleaner code. * Fix failing tests due to private method. * Moved action calls to later to fix failing tests. * Fix merge conflict error and failing tests. * Move is_product_subscription_renewal to private, remove direct tests due to it is tested elsewhere. * Remove duplicated code. --- .../multi-currency/class-compatibility.php | 109 +++++++++++++++++ .../class-currency-switcher-widget.php | 14 ++- .../multi-currency/class-frontend-prices.php | 30 +++-- .../multi-currency/class-multi-currency.php | 26 +++- includes/multi-currency/class-utils.php | 32 +++++ phpunit.xml.dist | 1 + .../helpers/class-wc-helper-subscriptions.php | 18 +++ .../test-class-compatibility.php | 112 ++++++++++++++++++ .../test-class-currency-switcher-widget.php | 23 +++- .../test-class-frontend-prices.php | 28 ++++- .../unit/multi-currency/test-class-utils.php | 35 ++++++ 11 files changed, 413 insertions(+), 15 deletions(-) create mode 100644 includes/multi-currency/class-compatibility.php create mode 100644 includes/multi-currency/class-utils.php create mode 100644 tests/unit/multi-currency/test-class-compatibility.php create mode 100644 tests/unit/multi-currency/test-class-utils.php diff --git a/includes/multi-currency/class-compatibility.php b/includes/multi-currency/class-compatibility.php new file mode 100644 index 00000000000..f43a02dd701 --- /dev/null +++ b/includes/multi-currency/class-compatibility.php @@ -0,0 +1,109 @@ +utils = $utils; + } + + /** + * Checks to see if the if the selected currency needs to be overridden. + * + * @return mixed Three letter currency code or false if not. + */ + public function override_selected_currency() { + $subscription_renewal = $this->cart_contains_renewal(); + if ( $subscription_renewal ) { + return get_post_meta( $subscription_renewal['subscription_renewal']['renewal_order_id'], '_order_currency', true ); + } + + return false; + } + + /** + * Checks to see if the widgets should be hidden. + * + * @return bool False if it shouldn't be hidden, true if it should. + */ + public function should_hide_widgets(): bool { + return $this->cart_contains_renewal() ? true : false; + } + + /** + * Checks to see if the product's price should be converted. + * + * @param object $product Product object to test. + * + * @return bool True if it should be converted. + */ + public function should_convert_product_price( $product = null ): bool { + // If we have a product, and it's a subscription renewal. + if ( $product && $this->is_product_subscription_renewal( $product ) ) { + $calls = [ + 'WC_Cart_Totals->calculate_item_totals', + 'WC_Cart->get_product_subtotal', + 'wc_get_price_excluding_tax', + 'wc_get_price_including_tax', + ]; + if ( $this->utils->is_call_in_backtrace( $calls ) ) { + return false; + } + } + + return true; + } + + /** + * Checks the cart to see if it contains a subscription product renewal. + * + * @return mixed The cart item containing the renewal as an array, else false. + */ + private function cart_contains_renewal() { + if ( ! function_exists( 'wcs_cart_contains_renewal' ) ) { + return false; + } + return wcs_cart_contains_renewal(); + } + + /** + * Checks to see if the product passed is in the cart as a subscription renewal. + * + * @param object $product Product to test. + * + * @return bool True if it's a subscription renewal in the cart, false if not. + */ + private function is_product_subscription_renewal( $product ): bool { + $subscription_renewal = $this->cart_contains_renewal(); + if ( $subscription_renewal && $product ) { + if ( ( isset( $subscription_renewal['variation_id'] ) && $subscription_renewal['variation_id'] === $product->get_id() ) + || $subscription_renewal['product_id'] === $product->get_id() ) { + return true; + } + } + return false; + } +} diff --git a/includes/multi-currency/class-currency-switcher-widget.php b/includes/multi-currency/class-currency-switcher-widget.php index 6d1704defcc..f571452a21b 100644 --- a/includes/multi-currency/class-currency-switcher-widget.php +++ b/includes/multi-currency/class-currency-switcher-widget.php @@ -22,6 +22,13 @@ class Currency_Switcher_Widget extends WP_Widget { 'flag' => false, ]; + /** + * Compatibility instance. + * + * @var Compatibility + */ + protected $compatibility; + /** * Multi-Currency instance. * @@ -33,9 +40,11 @@ class Currency_Switcher_Widget extends WP_Widget { * Register widget with WordPress. * * @param Multi_Currency $multi_currency The Multi_Currency instance. + * @param Compatibility $compatibility The Compatibility instance. */ - public function __construct( Multi_Currency $multi_currency ) { + public function __construct( Multi_Currency $multi_currency, Compatibility $compatibility ) { $this->multi_currency = $multi_currency; + $this->compatibility = $compatibility; parent::__construct( 'currency_switcher_widget', @@ -51,6 +60,9 @@ public function __construct( Multi_Currency $multi_currency ) { * @param array $instance Saved values from database. */ public function widget( $args, $instance ) { + if ( $this->compatibility->should_hide_widgets() ) { + return; + } $instance = wp_parse_args( $instance, self::DEFAULT_SETTINGS diff --git a/includes/multi-currency/class-frontend-prices.php b/includes/multi-currency/class-frontend-prices.php index d26b03aa292..ffb80ec3da7 100644 --- a/includes/multi-currency/class-frontend-prices.php +++ b/includes/multi-currency/class-frontend-prices.php @@ -13,6 +13,13 @@ * Class that applies Multi Currency prices on the frontend. */ class Frontend_Prices { + /** + * Compatibility instance. + * + * @var Compatibility + */ + protected $compatibility; + /** * Multi-Currency instance. * @@ -24,20 +31,22 @@ class Frontend_Prices { * Constructor. * * @param Multi_Currency $multi_currency The Multi_Currency instance. + * @param Compatibility $compatibility The Compatibility instance. */ - public function __construct( Multi_Currency $multi_currency ) { + public function __construct( Multi_Currency $multi_currency, Compatibility $compatibility ) { $this->multi_currency = $multi_currency; + $this->compatibility = $compatibility; if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) { // Simple product price hooks. - add_filter( 'woocommerce_product_get_price', [ $this, 'get_product_price' ], 50 ); - add_filter( 'woocommerce_product_get_regular_price', [ $this, 'get_product_price' ], 50 ); - add_filter( 'woocommerce_product_get_sale_price', [ $this, 'get_product_price' ], 50 ); + add_filter( 'woocommerce_product_get_price', [ $this, 'get_product_price' ], 50, 2 ); + add_filter( 'woocommerce_product_get_regular_price', [ $this, 'get_product_price' ], 50, 2 ); + add_filter( 'woocommerce_product_get_sale_price', [ $this, 'get_product_price' ], 50, 2 ); // Variation price hooks. - add_filter( 'woocommerce_product_variation_get_price', [ $this, 'get_product_price' ], 50 ); - add_filter( 'woocommerce_product_variation_get_regular_price', [ $this, 'get_product_price' ], 50 ); - add_filter( 'woocommerce_product_variation_get_sale_price', [ $this, 'get_product_price' ], 50 ); + add_filter( 'woocommerce_product_variation_get_price', [ $this, 'get_product_price' ], 50, 2 ); + add_filter( 'woocommerce_product_variation_get_regular_price', [ $this, 'get_product_price' ], 50, 2 ); + add_filter( 'woocommerce_product_variation_get_sale_price', [ $this, 'get_product_price' ], 50, 2 ); // Variation price range hooks. add_filter( 'woocommerce_variation_prices', [ $this, 'get_variation_price_range' ], 50 ); @@ -65,14 +74,19 @@ public function __construct( Multi_Currency $multi_currency ) { * Returns the price for a product. * * @param mixed $price The product's price. + * @param mixed $product WC_Product or null. * * @return mixed The converted product's price. */ - public function get_product_price( $price ) { + public function get_product_price( $price, $product = null ) { if ( ! $price ) { return $price; } + if ( ! $this->compatibility->should_convert_product_price( $product ) ) { + return $price; + } + return $this->multi_currency->get_price( $price, 'product' ); } diff --git a/includes/multi-currency/class-multi-currency.php b/includes/multi-currency/class-multi-currency.php index 10baeda753b..03abcb21b96 100644 --- a/includes/multi-currency/class-multi-currency.php +++ b/includes/multi-currency/class-multi-currency.php @@ -37,6 +37,13 @@ class Multi_Currency { */ protected static $instance = null; + /** + * Compatibility instance. + * + * @var Compatibility + */ + protected $compatibility; + /** * Frontend_Prices instance. * @@ -104,6 +111,8 @@ public function __construct( WC_Payments_API_Client $payments_api_client ) { $this->includes(); $this->payments_api_client = $payments_api_client; + $this->utils = new Utils(); + $this->compatibility = new Compatibility( $this->utils ); add_action( 'init', [ $this, 'init' ] ); add_action( 'rest_api_init', [ $this, 'init_rest_api' ] ); @@ -128,7 +137,7 @@ public function init() { new User_Settings( $this ); - $this->frontend_prices = new Frontend_Prices( $this ); + $this->frontend_prices = new Frontend_Prices( $this, $this->compatibility ); $this->frontend_currencies = new Frontend_Currencies( $this ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); @@ -153,7 +162,7 @@ public function init_rest_api() { * Initialize the Widgets. */ public function init_widgets() { - register_widget( new Currency_Switcher_Widget( $this ) ); + register_widget( new Currency_Switcher_Widget( $this, $this->compatibility ) ); } /** @@ -264,6 +273,15 @@ public function get_cached_currencies() { ]; } + /** + * Returns the Compatibility instance. + * + * @return Compatibility + */ + public function get_compatibility() { + return $this->compatibility; + } + /** * Returns the Frontend_Prices instance. * @@ -414,6 +432,8 @@ public function get_selected_currency(): Currency { $code = get_user_meta( $user_id, self::CURRENCY_META_KEY, true ); } + $code = $this->compatibility->override_selected_currency() ? $this->compatibility->override_selected_currency() : $code; + return $this->get_enabled_currencies()[ $code ] ?? $this->default_currency; } @@ -571,12 +591,14 @@ protected function ceil_price( float $price, float $rounding ): float { * Include required core files used in admin and on the frontend. */ protected function includes() { + include_once WCPAY_ABSPATH . 'includes/multi-currency/class-compatibility.php'; include_once WCPAY_ABSPATH . 'includes/multi-currency/class-currency.php'; include_once WCPAY_ABSPATH . 'includes/multi-currency/class-currency-switcher-widget.php'; include_once WCPAY_ABSPATH . 'includes/multi-currency/class-country-flags.php'; include_once WCPAY_ABSPATH . 'includes/multi-currency/class-frontend-prices.php'; include_once WCPAY_ABSPATH . 'includes/multi-currency/class-frontend-currencies.php'; include_once WCPAY_ABSPATH . 'includes/multi-currency/class-user-settings.php'; + include_once WCPAY_ABSPATH . 'includes/multi-currency/class-utils.php'; } /** diff --git a/includes/multi-currency/class-utils.php b/includes/multi-currency/class-utils.php new file mode 100644 index 00000000000..9cf701467d5 --- /dev/null +++ b/includes/multi-currency/class-utils.php @@ -0,0 +1,32 @@ +./tests/unit/multi-currency + ./tests/unit/helpers ./tests/unit/multi-currency diff --git a/tests/unit/helpers/class-wc-helper-subscriptions.php b/tests/unit/helpers/class-wc-helper-subscriptions.php index 84f8030b14c..76fff4dd7d5 100644 --- a/tests/unit/helpers/class-wc-helper-subscriptions.php +++ b/tests/unit/helpers/class-wc-helper-subscriptions.php @@ -34,6 +34,13 @@ function wcs_get_subscription( $subscription ) { return ( WC_Subscriptions::$wcs_get_subscription )( $subscription ); } +function wcs_cart_contains_renewal() { + if ( ! WC_Subscriptions::$wcs_cart_contains_renewal ) { + return; + } + return ( WC_Subscriptions::$wcs_cart_contains_renewal )(); +} + /** * Class WC_Subscriptions. * @@ -75,6 +82,13 @@ class WC_Subscriptions { */ public static $wcs_get_subscription = null; + /** + * wcs_cart_contains_renewal mock. + * + * @var function + */ + public static $wcs_cart_contains_renewal = null; + public static function set_wcs_order_contains_subscription( $function ) { self::$wcs_order_contains_subscription = $function; } @@ -90,4 +104,8 @@ public static function set_wcs_is_subscription( $function ) { public static function set_wcs_get_subscription( $function ) { self::$wcs_get_subscription = $function; } + + public static function wcs_cart_contains_renewal( $function ) { + self::$wcs_cart_contains_renewal = $function; + } } diff --git a/tests/unit/multi-currency/test-class-compatibility.php b/tests/unit/multi-currency/test-class-compatibility.php new file mode 100644 index 00000000000..d9bb1921e08 --- /dev/null +++ b/tests/unit/multi-currency/test-class-compatibility.php @@ -0,0 +1,112 @@ +mock_utils = $this->createMock( WCPay\Multi_Currency\Utils::class ); + $this->compatibility = new WCPay\Multi_Currency\Compatibility( $this->mock_utils ); + + $this->mock_product = $this->createMock( \WC_Product::class ); + $this->mock_product + ->method( 'get_id' ) + ->willReturn( 42 ); + } + + public function test_override_selected_currency_return_false() { + $this->mock_wcs_cart_contains_renewal( false ); + $this->assertFalse( $this->compatibility->override_selected_currency() ); + } + + public function test_override_selected_currency_return_true() { + $this->mock_wcs_cart_contains_renewal( true ); + update_post_meta( 42, '_order_currency', 'CAD', true ); + $this->assertSame( 'CAD', $this->compatibility->override_selected_currency() ); + } + + public function test_should_hide_widgets_return_false() { + $this->mock_wcs_cart_contains_renewal( false ); + $this->assertFalse( $this->compatibility->should_hide_widgets() ); + } + + public function test_should_hide_widgets_return_true() { + $this->mock_wcs_cart_contains_renewal( true ); + $this->assertTrue( $this->compatibility->should_hide_widgets() ); + } + + public function test_should_convert_product_price_return_false_when_renewal_in_cart() { + $this->mock_utils + ->method( 'is_call_in_backtrace' ) + ->with( + [ + 'WC_Cart_Totals->calculate_item_totals', + 'WC_Cart->get_product_subtotal', + 'wc_get_price_excluding_tax', + 'wc_get_price_including_tax', + ] + ) + ->willReturn( true ); + $this->mock_wcs_cart_contains_renewal( true ); + $this->assertFalse( $this->compatibility->should_convert_product_price( $this->mock_product ) ); + } + + public function test_should_convert_product_price_return_true_when_backtrace_does_not_match() { + $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( false ); + $this->mock_wcs_cart_contains_renewal( true ); + $this->assertTrue( $this->compatibility->should_convert_product_price( $this->mock_product ) ); + } + + public function test_should_convert_product_price_return_true_with_no_renewal_in_cart() { + $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( true ); + $this->mock_wcs_cart_contains_renewal( false ); + $this->assertTrue( $this->compatibility->should_convert_product_price( $this->mock_product ) ); + } + + public function test_should_convert_product_price_return_true_when_product_null() { + $this->mock_utils->method( 'is_call_in_backtrace' )->willReturn( true ); + $this->mock_wcs_cart_contains_renewal( true ); + $this->assertTrue( $this->compatibility->should_convert_product_price( null ) ); + } + + private function mock_wcs_cart_contains_renewal( $value ) { + WC_Subscriptions::wcs_cart_contains_renewal( + function () use ( $value ) { + if ( $value ) { + return [ + 'product_id' => 42, + 'subscription_renewal' => [ + 'renewal_order_id' => 42, + ], + ]; + } + + return false; + } + ); + } +} diff --git a/tests/unit/multi-currency/test-class-currency-switcher-widget.php b/tests/unit/multi-currency/test-class-currency-switcher-widget.php index 8e93e74467d..55c311eacf9 100644 --- a/tests/unit/multi-currency/test-class-currency-switcher-widget.php +++ b/tests/unit/multi-currency/test-class-currency-switcher-widget.php @@ -11,6 +11,13 @@ * WCPay\Multi_Currency\Currency_Switcher_Widget unit tests. */ class WCPay_Multi_Currency_Currency_Switcher_Widget_Tests extends WP_UnitTestCase { + /** + * Mock WCPay\Multi_Currency\Compatibility. + * + * @var WCPay\Multi_Currency\Compatibility|PHPUnit_Framework_MockObject_MockObject + */ + private $mock_compatibility; + /** * Mock WCPay\Multi_Currency\Multi_Currency. * @@ -31,6 +38,7 @@ class WCPay_Multi_Currency_Currency_Switcher_Widget_Tests extends WP_UnitTestCas public function setUp() { parent::setUp(); + $this->mock_compatibility = $this->createMock( WCPay\Multi_Currency\Compatibility::class ); $this->mock_multi_currency = $this->createMock( WCPay\Multi_Currency\Multi_Currency::class ); $this->mock_multi_currency ->method( 'get_enabled_currencies' ) @@ -42,11 +50,11 @@ public function setUp() { ] ); - $this->currency_switcher_widget = new WCPay\Multi_Currency\Currency_Switcher_Widget( $this->mock_multi_currency ); + $this->currency_switcher_widget = new WCPay\Multi_Currency\Currency_Switcher_Widget( $this->mock_multi_currency, $this->mock_compatibility ); } - public function test_widget_renders_title_with_args() { + $this->mock_compatibility->method( 'should_hide_widgets' )->willReturn( false ); $instance = [ 'title' => 'Test Title', ]; @@ -58,6 +66,7 @@ public function test_widget_renders_title_with_args() { } public function test_widget_renders_enabled_currencies_with_symbol() { + $this->mock_compatibility->method( 'should_hide_widgets' )->willReturn( false ); $this->render_widget(); $this->expectOutputRegex( '/