diff --git a/assets/css/about.css b/assets/css/about.css old mode 100755 new mode 100644 diff --git a/assets/css/admin.css b/assets/css/admin.css old mode 100755 new mode 100644 diff --git a/assets/css/checkout.css b/assets/css/checkout.css old mode 100755 new mode 100644 diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css old mode 100755 new mode 100644 diff --git a/assets/css/view-subscription.css b/assets/css/view-subscription.css old mode 100755 new mode 100644 diff --git a/assets/css/wcs-upgrade.css b/assets/css/wcs-upgrade.css old mode 100755 new mode 100644 diff --git a/assets/images/add-edit-subscription-screen.png b/assets/images/add-edit-subscription-screen.png old mode 100755 new mode 100644 diff --git a/assets/images/admin-change-payment-method.jpg b/assets/images/admin-change-payment-method.jpg old mode 100755 new mode 100644 diff --git a/assets/images/ajax-loader.gif b/assets/images/ajax-loader.gif old mode 100755 new mode 100644 diff --git a/assets/images/ajax-loader@2x.gif b/assets/images/ajax-loader@2x.gif old mode 100755 new mode 100644 diff --git a/assets/images/billing-schedules-meta-box.png b/assets/images/billing-schedules-meta-box.png old mode 100755 new mode 100644 diff --git a/assets/images/checkout-recurring-totals.png b/assets/images/checkout-recurring-totals.png old mode 100755 new mode 100644 diff --git a/assets/images/drip-downloadable-content.jpg b/assets/images/drip-downloadable-content.jpg old mode 100755 new mode 100644 diff --git a/assets/images/gift-subscription.png b/assets/images/gift-subscription.png old mode 100755 new mode 100644 diff --git a/assets/images/renewal-retry-settings.png b/assets/images/renewal-retry-settings.png old mode 100755 new mode 100644 diff --git a/assets/images/subscribe-all-the-things.png b/assets/images/subscribe-all-the-things.png old mode 100755 new mode 100644 diff --git a/assets/images/subscription-reports.png b/assets/images/subscription-reports.png old mode 100755 new mode 100644 diff --git a/assets/images/subscription-suspended-email.jpg b/assets/images/subscription-suspended-email.jpg old mode 100755 new mode 100644 diff --git a/assets/images/subscriptions-importer-exporter.png b/assets/images/subscriptions-importer-exporter.png old mode 100755 new mode 100644 diff --git a/assets/images/view-subscription.png b/assets/images/view-subscription.png old mode 100755 new mode 100644 diff --git a/assets/images/woocommerce_subscriptions_logo.png b/assets/images/woocommerce_subscriptions_logo.png old mode 100755 new mode 100644 diff --git a/assets/js/admin/admin-pointers.js b/assets/js/admin/admin-pointers.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/admin.js b/assets/js/admin/admin.js old mode 100755 new mode 100644 index 7c0b49b..d0eaff7 --- a/assets/js/admin/admin.js +++ b/assets/js/admin/admin.js @@ -312,7 +312,7 @@ jQuery(document).ready(function($){ }); // if we haven't found a variation synced or with a trial at this point check the backend for other product variations - if ( ( number_of_pages > 1 || 0 == variations.size() ) && false == is_synced_or_has_trial ) { + if ( ( number_of_pages > 1 || 0 == variations.length ) && false == is_synced_or_has_trial ) { var data = { action: 'wcs_product_has_trial_or_is_synced', diff --git a/assets/js/admin/jquery.flot.axislabels.js b/assets/js/admin/jquery.flot.axislabels.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/jquery.flot.axislabels.min.js b/assets/js/admin/jquery.flot.axislabels.min.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/jquery.flot.orderBars.js b/assets/js/admin/jquery.flot.orderBars.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/jquery.flot.orderBars.min.js b/assets/js/admin/jquery.flot.orderBars.min.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/jstz.js b/assets/js/admin/jstz.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/jstz.min.js b/assets/js/admin/jstz.min.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/meta-boxes-coupon.js b/assets/js/admin/meta-boxes-coupon.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/meta-boxes-subscription.js b/assets/js/admin/meta-boxes-subscription.js old mode 100755 new mode 100644 index 871a3bd..bfc00cc --- a/assets/js/admin/meta-boxes-subscription.js +++ b/assets/js/admin/meta-boxes-subscription.js @@ -58,21 +58,8 @@ jQuery(document).ready(function($){ seconds: one_hour_from_now.format( 'ss' ) }); - - // Make sure start date is before now - if ( 'start' == date_type ) { - - if ( false === chosen_date.isBefore( time_now ) ) { - alert( wcs_admin_meta_boxes.i18n_start_date_notice ); - $date_input.val( time_now.year() + '-' + ( zeroise( time_now.months() + 1 ) ) + '-' + ( time_now.format( 'DD' ) ) ); - $hour_input.val( time_now.format( 'HH' ) ); - $minute_input.val( time_now.format( 'mm' ) ); - } - - } - // Make sure trial end and next payment are after start date - else if ( ( 'trial_end' == date_type || 'next_payment' == date_type ) && '' != $( '#start_timestamp_utc' ).val() ) { + if ( ( 'trial_end' == date_type || 'next_payment' == date_type ) && '' != $( '#start_timestamp_utc' ).val() ) { var change_date = false, start = moment.unix( $('#start_timestamp_utc').val() ); diff --git a/assets/js/admin/moment.js b/assets/js/admin/moment.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/moment.min.js b/assets/js/admin/moment.min.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/reports.js b/assets/js/admin/reports.js old mode 100755 new mode 100644 diff --git a/assets/js/admin/wcs-meta-boxes-order.js b/assets/js/admin/wcs-meta-boxes-order.js old mode 100755 new mode 100644 diff --git a/assets/js/frontend/single-product.js b/assets/js/frontend/single-product.js new file mode 100644 index 0000000..12b0d6c --- /dev/null +++ b/assets/js/frontend/single-product.js @@ -0,0 +1,52 @@ +(function ( document, $ ) { + + var $cache = {}; + + /** + * Cache our DOM selectors. + */ + function generate_cache() { + $cache.document = $( document ); + $cache.first_payment_date = $( '.first-payment-date' ); + $cache.is_variable_subscription = 0 < $( 'div.product-type-variable-subscription' ).length; + } + + /** + * Attach DOM events. + */ + function attach_events() { + if ( $cache.is_variable_subscription ) { + $cache.document.on( 'found_variation', update_first_payment_element ); + $cache.document.on( 'reset_data', clear_first_payment_element ); + } + } + + /** + * Update the variation's first payment element. + * + * @param {jQuery.Event} event + * @param {object} variation_data + */ + function update_first_payment_element( event, variation_data ) { + $cache.first_payment_date.html( variation_data.first_payment_html ); + } + + /** + * Clear the variation's first payment element. + */ + function clear_first_payment_element() { + $cache.first_payment_date.html( '' ); + } + + /** + * Initialise. + */ + function init() { + generate_cache(); + attach_events(); + } + + $( init ); + +})( document, jQuery ); + diff --git a/assets/js/wcs-upgrade.js b/assets/js/wcs-upgrade.js old mode 100755 new mode 100644 diff --git a/changelog.txt b/changelog.txt old mode 100755 new mode 100644 index e7807c5..ba82742 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,70 @@ *** WooCommerce Subscriptions Changelog *** +2018.11.28 - version 2.4.5 +* Fix: Fix caching on data for the Upcoming Recurring Revenue and Subscriptions By Date reports. PR#3069. +* Fix: Fix compatibility with Disable Report Cache Updates mini-plugin. PR#3069. +* Fix: Make Stripe source ID unique while setting through REST API. PR#3090. +* Fix: Display synced variation product's next payment date after the variation is selected by a customer on the product page. PR#3094. +* Fix: Activate subscription after processing an IPN on parent orders previously processed by PDT. PR#3097. +* Tweak: Update incomplete retry migration status label styling and wording to "In-Progress" to avoid confusing in-progress migrations as being a problem. PR#3088 + +2018.11.20 - version 2.4.4 +* Fix: Prevent fatal errors when the retry migration doesn't receive a valid retry object. PR#3042 +* Fix: Fix PHP notices in subscription events by date report. PR#3078 +* Fix: Don't trigger payment gateways while processing retries on staging sites. PR#2870 +* Fix: Apply coupons and discounts to initial payment and early renewal carts. PR#3048 +* Fix: Use the correct `for` attribute in the variable_subscription_trial_period label element. PR#3081 +* Fix: [PayPal Standard] Prevent creating multiple orders to record a single sign up transaction that was previously handled by a PDT request. PR#3074 +* Tweak: Remove the nonce from early renewal urls. PR#3085 + +2018.11.13 - version 2.4.3 +* Fix: Pass the quantity argument to woocommerce_add_cart_item_data filter. PR#3051 +* Dev: Update Action Scheduler to 2.1.1. PR#3059 +* Dev: Add a new filter (`wcs_remove_fees_from_initial_cart`) which can be used to override the removal of fees from the initial cart. PR#3046 + +2018.10.29 - version 2.4.2 +* Fix: Always get the subscription's customer user from post meta. Fixes issues on sites running WC 3.5 where the subscription customer was incorrectly being set to an admin user. PR#3037 +* Fix: Use the current timestamp of site's timezone for comparison during synchronisation first payment date calculations. PR#3003 +* Fix: Sort the user subscriptions cache to ensure subscriptions appear in the correct order on the My Account > Subscriptions page. PR#2965 +* Fix: Correctly account for tax inclusive sign up fees for synced products. PR#2818 +* Fix: Fix PHP 7.1.x warnings related to get_sign_up_fee() and product variations. PR#3014 +* Fix: Use the correct breadcrumbs on "change payment method" and "change address" pages. PR#3008 +* Fix: Fix product purchasability issues when switching between limited variable products. PR#3007 +* Fix: Remove the erasure tools link from the description of the erasure settings for users without the manage_privacy_options capability. PR#3004 +* Dev: Add switch proration filters. PR#3011 +* Dev: Use admin_created_subscription hook to generate download permissions. PR#3024 +* Dev: Deprecated wcs_get_sites_timezone() fixes get_local_timezone() deprecated warnings. PR#3032 + +2018.10.16 - version 2.4.1 +* Fix: Cache the retry data needs migration logic to avoid a get_posts() call on every request. PR#2994 +* Fix: Load the retry store instance on WordPress init rather than on Subscriptions plugin load. Fixes errors with third-party plugins hooking into get post queries and calling undeclared WP functions. PR#3000 +* Fix: Fix an undefined product ID error which could occur during a switch request when trying to access a product from the old subscription format. PR#2961 +* Fix: Fix a missing closing `a` tag in the downgrade notice. PR#3002 +* Dev: Add a new hook ('woocommerce_admin_created_subscription') which is triggered after a subscription is created manually via the admin screen. PR#2988 +* Dev: Add new hooks to allow third-parties to customize subscription filtering behavior on the admin subscriptions table. PR#2989 + +2018.10.09 - version 2.4.0 +* New: Update Action Scheduler to v2.1.0. +* Fix: Remove the URL scheme when generating the site's duplicate lock URL. Fixes an issue where staging sites with a URL length equal the scheme would incorrectly not trigger staging mode. PR#2599 +* Fix: Move callbacks attached specifically for renewal carts to the constructor so that all extended cart classes are not hooked on. PR#2963 +* Fix: Allow products which are password protected to be manually renewed via the cart. PR#2912 +* Fix: Remove unnecessary leading space in translation string. PR#2976 +* Fix: Add group by clause to subscriber count reports query. PR#2970 +* Fix: Fix an error which would occur when calling the deprecated get_related_orders_query(). PR#2954 +* Tweak: Separate a subscription start from the creation date. PR#2594 +* Tweak: Display the expected site URL in staging site notice and system status. PR#2950 +* Dev: Implement an Autoloader for loading plugin class files. PR#2412 +* Dev: Replace WC_Logger type hinting in function signatures to use lower level WC_Logger_Interface. PR#2902 +* Dev: Remove switch_line_items_pre_2_1_2() and switch_shipping_line_items_pre_2_1_2() functions. PR#2900 +* Dev: [WC 3.5] Move subscription customer ID's into the post author column on sites running WC 3.5 and above. PR#2559 +* Dev: Move payment retry data to custom tables. PR#2753 +* Dev: Update file inclusions to use relative paths. PR#2936 +* Dev: Add an API for third-parties to register their custom subscription date types. PR#2840 +* Dev: Update the hook used for adding Subscriptions Settings. PR#2939 +* Dev: Add a 'wcs_payment_gateways_change_payment_method' filter to allow third-parties to filter which payment methods are displayed when changing payment methods. PR#2571 +* Dev: Trigger a new 'wcs_before_replace_pay_shortcode' action before displaying the change payment shortcode. +* Dev: Add the 'wcs_update_all_subscriptions_addresses_checked' filter to allow third-parties to filter the default checked status of the all_subscriptions_addresses field. PR#2948 + 2018.09.25 - version 2.3.7 * Fix: Don't apply subscription payment gateway filters while on the order-pay page. PR#2927 * Fix: Prevent admin users from changing product type when the product has active subscriptions. PR#2932 diff --git a/composer.lock b/composer.lock old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-background-updater.php b/includes/abstracts/abstract-wcs-background-updater.php old mode 100755 new mode 100644 index 513cd7f..a251cf7 --- a/includes/abstracts/abstract-wcs-background-updater.php +++ b/includes/abstracts/abstract-wcs-background-updater.php @@ -125,8 +125,8 @@ abstract class WCS_Background_Updater { * Schedule the instance's hook to run in $this->time_limit seconds, if it's not already scheduled. */ protected function schedule_background_update() { - if ( false === wc_next_scheduled_action( $this->scheduled_hook ) ) { - wc_schedule_single_action( gmdate( 'U' ) + $this->time_limit, $this->scheduled_hook ); + if ( false === as_next_scheduled_action( $this->scheduled_hook ) ) { + as_schedule_single_action( gmdate( 'U' ) + $this->time_limit, $this->scheduled_hook ); } } @@ -134,7 +134,7 @@ abstract class WCS_Background_Updater { * Unschedule the instance's hook in Action Scheduler */ protected function unschedule_background_updates() { - wc_unschedule_action( $this->scheduled_hook ); + as_unschedule_action( $this->scheduled_hook ); } /** diff --git a/includes/abstracts/abstract-wcs-background-upgrader.php b/includes/abstracts/abstract-wcs-background-upgrader.php old mode 100755 new mode 100644 index 8b1bf2e..f816ea0 --- a/includes/abstracts/abstract-wcs-background-upgrader.php +++ b/includes/abstracts/abstract-wcs-background-upgrader.php @@ -20,7 +20,7 @@ abstract class WCS_Background_Upgrader extends WCS_Background_Updater { /** * WC Logger instance for logging messages. * - * @var WC_Logger + * @var WC_Logger_Interface */ protected $logger; diff --git a/includes/abstracts/abstract-wcs-cache-manager.php b/includes/abstracts/abstract-wcs-cache-manager.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-customer-store.php b/includes/abstracts/abstract-wcs-customer-store.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-debug-tool-cache-updater.php b/includes/abstracts/abstract-wcs-debug-tool-cache-updater.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-debug-tool.php b/includes/abstracts/abstract-wcs-debug-tool.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-dynamic-hook-deprecator.php b/includes/abstracts/abstract-wcs-dynamic-hook-deprecator.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-hook-deprecator.php b/includes/abstracts/abstract-wcs-hook-deprecator.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-migrator.php b/includes/abstracts/abstract-wcs-migrator.php new file mode 100644 index 0000000..c86fe4f --- /dev/null +++ b/includes/abstracts/abstract-wcs-migrator.php @@ -0,0 +1,133 @@ +source_store = $source_store; + $this->destination_store = $destination_store; + $this->logger = $logger; + } + + /** + * Should this entry be migrated. + * + * @param int $entry_id + * + * @return bool + * @since 2.4 + */ + abstract public function should_migrate_entry( $entry_id ); + + /** + * Gets the item from the source store. + * + * @param int $entry_id + * + * @return mixed + * @since 2.4 + */ + abstract public function get_source_store_entry( $entry_id ); + + /** + * save the item to the destination store. + * + * @param int $entry_id + * + * @return mixed + * @since 2.4 + */ + abstract public function save_destination_store_entry( $entry_id ); + + /** + * deletes the item from the source store. + * + * @param int $entry_id + * + * @return mixed + * @since 2.4 + */ + abstract public function delete_source_store_entry( $entry_id ); + + /** + * Runs after a entry has been migrated. + * + * @param int $old_entry_id + * @param mixed $new_entry + * + * @return mixed + */ + abstract protected function migrated_entry( $old_entry_id, $new_entry ); + + /** + * Migrates our entry. + * + * @param int $entry_id + * + * @return mixed + * @since 2.4 + */ + public function migrate_entry( $entry_id ) { + $source_store_item = $this->get_source_store_entry( $entry_id ); + if ( $source_store_item ) { + $destination_store_item = $this->save_destination_store_entry( $entry_id ); + $this->delete_source_store_entry( $entry_id ); + + $this->migrated_entry( $entry_id, $destination_store_item ); + + return $destination_store_item; + } + + return false; + } + + /** + * Add a message to the log + * + * @param string $message The message to be logged + * + * @since 2.4.0 + */ + protected function log( $message ) { + $this->logger->add( $this->log_handle, $message ); + } +} diff --git a/includes/abstracts/abstract-wcs-related-order-store.php b/includes/abstracts/abstract-wcs-related-order-store.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-retry-store.php b/includes/abstracts/abstract-wcs-retry-store.php old mode 100755 new mode 100644 index 05d7d88..b1cb4a5 --- a/includes/abstracts/abstract-wcs-retry-store.php +++ b/includes/abstracts/abstract-wcs-retry-store.php @@ -2,11 +2,11 @@ /** * An interface for creating a store for retry details. * - * @package WooCommerce Subscriptions - * @subpackage WCS_Retry_Store - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WCS_Retry_Store + * @category Class + * @author Prospress + * @since 2.1 */ abstract class WCS_Retry_Store { @@ -18,6 +18,7 @@ abstract class WCS_Retry_Store { * Save the details of a retry to the database * * @param WCS_Retry $retry + * * @return int the retry's ID */ abstract public function save( WCS_Retry $retry ); @@ -26,27 +27,56 @@ abstract class WCS_Retry_Store { * Get the details of a retry from the database * * @param int $retry_id + * * @return WCS_Retry */ abstract public function get_retry( $retry_id ); + /** + * Deletes a retry. + * + * @param int $retry_id + * + * @since 2.4 + */ + public function delete_retry( $retry_id ) { + wcs_doing_it_wrong( __FUNCTION__, sprintf( "Method '%s' must be overridden.", __METHOD__ ), '2.4' ); + } + /** * Get a set of retries from the database * - * @param array $args A set of filters: - * 'status': filter to only retries of a certain status, either 'pending', 'processing', 'failed' or 'complete'. Default: 'any', which will return all retries. - * 'date_query': array of dates to filter retries those that occur 'after' or 'before' a certain (or inbetween those two dates). Should be a MySQL formated date/time string. - * @return array An array of WCS_Retry objects + * @param array $args A set of filters: + * 'status': filter to only retries of a certain status, either 'pending', 'processing', 'failed' or 'complete'. Default: 'any', which will return all retries. + * 'date_query': array of dates to filter retries to those that occur 'after' or 'before' a certain date (or between those two dates). Should be a MySQL formated date/time string. + * 'orderby': Order by which property? + * 'order': Order in ASC/DESC. + * 'order_id': filter retries to those which belong to a certain order ID. + * 'limit': How many retries we want to get. + * @param string $return Defines in which format return the entries. options: + * 'objects': Returns an array of WCS_Retry objects + * 'ids': Returns an array of ids. + * + * @return array An array of WCS_Retry objects or ids. + * @since 2.4 */ - abstract public function get_retries( $args ); + abstract public function get_retries( $args = array(), $return = 'objects' ); /** * Get the IDs of all retries from the database for a given order * * @param int $order_id + * * @return array + * @since 2.4 */ - abstract protected function get_retry_ids_for_order( $order_id ); + public function get_retry_ids_for_order( $order_id ) { + return array_values( $this->get_retries( array( + 'order_id' => $order_id, + 'orderby' => 'ID', + 'order' => 'ASC', + ), 'ids' ) ); + } /** * Setup the class, if required @@ -59,34 +89,28 @@ abstract class WCS_Retry_Store { * Get the details of all retries (if any) for a given order * * @param int $order_id + * * @return array */ public function get_retries_for_order( $order_id ) { - - $retries = array(); - - foreach ( $this->get_retry_ids_for_order( $order_id ) as $retry_id ) { - $retries[ $retry_id ] = $this->get_retry( $retry_id ); - } - - return $retries; + return $this->get_retries( array( 'order_id' => $order_id ) ); } /** * Get the details of the last retry (if any) recorded for a given order * * @param int $order_id + * * @return WCS_Retry | null */ public function get_last_retry_for_order( $order_id ) { - $retry_ids = $this->get_retry_ids_for_order( $order_id ); + $retry_ids = $this->get_retry_ids_for_order( $order_id ); + $last_retry = null; if ( ! empty( $retry_ids ) ) { $last_retry_id = array_pop( $retry_ids ); $last_retry = $this->get_retry( $last_retry_id ); - } else { - $last_retry = null; } return $last_retry; @@ -96,6 +120,7 @@ abstract class WCS_Retry_Store { * Get the number of retries stored in the database for a given order * * @param int $order_id + * * @return int */ public function get_retry_count_for_order( $order_id ) { diff --git a/includes/abstracts/abstract-wcs-scheduler.php b/includes/abstracts/abstract-wcs-scheduler.php old mode 100755 new mode 100644 diff --git a/includes/abstracts/abstract-wcs-table-maker.php b/includes/abstracts/abstract-wcs-table-maker.php new file mode 100644 index 0000000..7925530 --- /dev/null +++ b/includes/abstracts/abstract-wcs-table-maker.php @@ -0,0 +1,128 @@ +tables as $table ) { + $wpdb->tables[] = $table; + $name = $this->get_full_table_name( $table ); + $wpdb->$table = $name; + } + + // create the tables + if ( $this->schema_update_required() ) { + foreach ( $this->tables as $table ) { + $this->update_table( $table ); + } + $this->mark_schema_update_complete(); + } + } + + /** + * @param string $table The name of the table + * + * @return string The CREATE TABLE statement, suitable for passing to dbDelta + */ + abstract protected function get_table_definition( $table ); + + /** + * Determine if the database schema is out of date + * by comparing the integer found in $this->schema_version + * with the option set in the WordPress options table + * + * @return bool + */ + private function schema_update_required() { + $version_found_in_db = $this->get_schema_option(); + + return version_compare( $version_found_in_db, $this->schema_version, '<' ); + } + + /** + * Gets the schema version name. + * + * @return string + */ + private function get_schema_option_name() { + return 'wcs-schema-' . get_class( $this ); + } + + /** + * Gets the schema version we have. + * + * @return mixed + */ + private function get_schema_option() { + return get_option( $this->get_schema_option_name(), 0 ); + } + + /** + * Update the option in WordPress to indicate that + * our schema is now up to date + */ + private function mark_schema_update_complete() { + $option_name = $this->get_schema_option_name(); + + // work around race conditions and ensure that our option updates + $value_to_save = (string) $this->schema_version . '.0.' . time(); + + update_option( $option_name, $value_to_save ); + } + + /** + * Update the schema for the given table + * + * @param string $table The name of the table to update + */ + private function update_table( $table ) { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + $definition = $this->get_table_definition( $table ); + if ( $definition ) { + $updated = dbDelta( $definition ); + foreach ( $updated as $updated_table => $update_description ) { + if ( strpos( $update_description, 'Created table' ) === 0 ) { + do_action( 'wcs_created_table', $updated_table, $table ); + } + } + } + } + + /** + * @param string $table + * + * @return string The full name of the table, including the + * table prefix for the current blog + */ + protected function get_full_table_name( $table ) { + return $GLOBALS['wpdb']->prefix . $table; + } +} diff --git a/includes/admin/class-wc-subscriptions-admin.php b/includes/admin/class-wc-subscriptions-admin.php old mode 100755 new mode 100644 index 9dfc987..6cd4428 --- a/includes/admin/class-wc-subscriptions-admin.php +++ b/includes/admin/class-wc-subscriptions-admin.php @@ -92,7 +92,7 @@ class WC_Subscriptions_Admin { add_filter( 'woocommerce_settings_tabs_array', __CLASS__ . '::add_subscription_settings_tab', 50 ); - add_action( 'woocommerce_settings_tabs_subscriptions', __CLASS__ . '::subscription_settings_page' ); + add_action( 'woocommerce_settings_subscriptions', __CLASS__ . '::subscription_settings_page' ); add_action( 'woocommerce_update_options_' . self::$tab_name, __CLASS__ . '::update_subscription_settings' ); @@ -150,6 +150,10 @@ class WC_Subscriptions_Admin { 'wc_report_subscription_by_product', 'wc_report_subscription_by_customer', 'wc_report_subscription_events_by_date', + 'wcs_report_subscription_by_product', + 'wcs_report_subscription_by_customer', + 'wcs_report_subscription_events_by_date', + 'wcs_report_upcoming_recurring_revenue', ); // Get all related order and subscription ranges transients @@ -938,11 +942,6 @@ class WC_Subscriptions_Admin { public static function get_subscriptions_list_table() { if ( ! isset( self::$subscriptions_list_table ) ) { - - if ( ! class_exists( 'WC_Subscriptions_List_Table' ) ) { - require_once( 'class-wc-subscriptions-list-table.php' ); - } - self::$subscriptions_list_table = new WC_Subscriptions_List_Table(); } @@ -1744,7 +1743,7 @@ class WC_Subscriptions_Admin { */ public static function related_orders_meta_box( $post ) { _deprecated_function( __METHOD__, '2.0', 'WCS_Meta_Box_Related_Orders::output()' ); - WCS_Meta_Box_Related_Orders::output(); + WCS_Meta_Box_Related_Orders::output( $post ); } /** @@ -1800,5 +1799,3 @@ class WC_Subscriptions_Admin { _deprecated_function( __METHOD__, '2.0' ); } } - -WC_Subscriptions_Admin::init(); diff --git a/includes/admin/class-wcs-admin-meta-boxes.php b/includes/admin/class-wcs-admin-meta-boxes.php old mode 100755 new mode 100644 index 7a162df..12b4d61 --- a/includes/admin/class-wcs-admin-meta-boxes.php +++ b/includes/admin/class-wcs-admin-meta-boxes.php @@ -296,5 +296,3 @@ class WCS_Admin_Meta_Boxes { return $can_be_retried; } } - -new WCS_Admin_Meta_Boxes(); diff --git a/includes/admin/class-wcs-admin-notice.php b/includes/admin/class-wcs-admin-notice.php old mode 100755 new mode 100644 diff --git a/includes/admin/class-wcs-admin-post-types.php b/includes/admin/class-wcs-admin-post-types.php old mode 100755 new mode 100644 index 6be191b..4d9409a --- a/includes/admin/class-wcs-admin-post-types.php +++ b/includes/admin/class-wcs-admin-post-types.php @@ -13,7 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Exit if accessed directly if ( class_exists( 'WCS_Admin_Post_Types' ) ) { - return new WCS_Admin_Post_Types(); + return; } /** @@ -628,7 +628,7 @@ class WCS_Admin_Post_Types { */ public static function get_date_column_content( $subscription, $column ) { - $date_type_map = array( 'start_date' => 'date_created', 'last_payment_date' => 'last_order_date_created' ); + $date_type_map = array( 'last_payment_date' => 'last_order_date_created' ); $date_type = array_key_exists( $column, $date_type_map ) ? $date_type_map[ $column ] : $column; if ( 0 == $subscription->get_time( $date_type, 'gmt' ) ) { @@ -655,7 +655,7 @@ class WCS_Admin_Post_Types { $sortable_columns = array( 'order_title' => 'ID', 'recurring_total' => 'order_total', - 'start_date' => 'date', + 'start_date' => 'start_date', 'trial_end_date' => 'trial_end_date', 'next_payment_date' => 'next_payment_date', 'last_payment_date' => 'last_payment_date', @@ -746,13 +746,23 @@ class WCS_Admin_Post_Types { // Filter the orders by the posted customer. if ( isset( $_GET['_customer_user'] ) && $_GET['_customer_user'] > 0 ) { - $subscription_ids = WCS_Customer_Store::instance()->get_users_subscription_ids( absint( $_GET['_customer_user'] ) ); + $customer_id = absint( $_GET['_customer_user'] ); + $subscription_ids = apply_filters( + 'wcs_admin_request_query_subscriptions_for_customer', + WCS_Customer_Store::instance()->get_users_subscription_ids( $customer_id ), + $customer_id + ); $vars = self::set_post__in_query_var( $vars, $subscription_ids ); } if ( isset( $_GET['_wcs_product'] ) && $_GET['_wcs_product'] > 0 ) { - $subscription_ids = wcs_get_subscriptions_for_product( $_GET['_wcs_product'] ); - $subscription_ids = array_keys( $subscription_ids ); + $product_id = absint( $_GET['_wcs_product'] ); + $subscription_ids = wcs_get_subscriptions_for_product( $product_id ); + $subscription_ids = apply_filters( + 'wcs_admin_request_query_subscriptions_for_product', + array_keys( $subscription_ids ), + $product_id + ); $vars = self::set_post__in_query_var( $vars, $subscription_ids ); } @@ -804,6 +814,7 @@ class WCS_Admin_Post_Types { case 'last_payment_date' : add_filter( 'posts_clauses', array( $this, 'posts_clauses' ), 10, 2 ); break; + case 'start_date': case 'trial_end_date' : case 'next_payment_date' : case 'end_date' : @@ -1124,5 +1135,3 @@ class WCS_Admin_Post_Types { Reports admin section add_filter( 'woocommerce_admin_reports', __CLASS__ . '::initialize_reports', 12, 1 ); - // Add the reports layout to the WooCommerce -> Reports admin section - add_filter( 'wc_admin_reports_path', __CLASS__ . '::initialize_reports_path', 12, 3 ); - // Add any necessary scripts add_action( 'admin_enqueue_scripts', __CLASS__ . '::reports_scripts' ); // Add any actions we need based on the screen add_action( 'current_screen', __CLASS__ . '::conditional_reporting_includes' ); - } /** @@ -54,31 +49,31 @@ class WCS_Admin_Reports { 'title' => __( 'Subscription Events by Date', 'woocommerce-subscriptions' ), 'description' => '', 'hide_title' => true, - 'callback' => array( 'WC_Admin_Reports', 'get_report' ), + 'callback' => array( 'WCS_Admin_Reports', 'get_report' ), ), 'upcoming_recurring_revenue' => array( 'title' => __( 'Upcoming Recurring Revenue', 'woocommerce-subscriptions' ), 'description' => '', 'hide_title' => true, - 'callback' => array( 'WC_Admin_Reports', 'get_report' ), + 'callback' => array( 'WCS_Admin_Reports', 'get_report' ), ), 'retention_rate' => array( 'title' => __( 'Retention Rate', 'woocommerce-subscriptions' ), 'description' => '', 'hide_title' => true, - 'callback' => array( 'WC_Admin_Reports', 'get_report' ), + 'callback' => array( 'WCS_Admin_Reports', 'get_report' ), ), 'subscription_by_product' => array( 'title' => __( 'Subscriptions by Product', 'woocommerce-subscriptions' ), 'description' => '', 'hide_title' => true, - 'callback' => array( 'WC_Admin_Reports', 'get_report' ), + 'callback' => array( 'WCS_Admin_Reports', 'get_report' ), ), 'subscription_by_customer' => array( 'title' => __( 'Subscriptions by Customer', 'woocommerce-subscriptions' ), 'description' => '', 'hide_title' => true, - 'callback' => array( 'WC_Admin_Reports', 'get_report' ), + 'callback' => array( 'WCS_Admin_Reports', 'get_report' ), ), ), ); @@ -88,32 +83,13 @@ class WCS_Admin_Reports { 'title' => __( 'Failed Payment Retries', 'woocommerce-subscriptions' ), 'description' => '', 'hide_title' => true, - 'callback' => array( 'WC_Admin_Reports', 'get_report' ), + 'callback' => array( 'WCS_Admin_Reports', 'get_report' ), ); } return $reports; } - /** - * If we hit one of our reports in the WC get_report function, change the path to our dir. - * - * @param report_path the parth to the report. - * @param name the name of the report. - * @param class the class of the report. - * @return string path to the report template. - * @since 2.1 - */ - public static function initialize_reports_path( $report_path, $name, $class ) { - - if ( in_array( strtolower( $class ), array( 'wc_report_subscription_events_by_date', 'wc_report_upcoming_recurring_revenue', 'wc_report_retention_rate', 'wc_report_subscription_by_product', 'wc_report_subscription_by_customer', 'wc_report_subscription_payment_retry' ) ) ) { - $report_path = dirname( __FILE__ ) . '/reports/class-wcs-report-' . $name . '.php'; - } - - return $report_path; - - } - /** * Add any subscriptions report javascript to the admin pages. * @@ -157,11 +133,54 @@ class WCS_Admin_Reports { switch ( $screen->id ) { case 'dashboard' : - include_once( 'reports/class-wcs-report-dashboard.php' ); - break; + new WCS_Report_Dashboard(); + break; } } -} -new WCS_Admin_Reports(); + /** + * Get a report from one of our classes. + * + * @param string $name + */ + public static function get_report( $name ) { + $name = sanitize_title( str_replace( '_', '-', $name ) ); + $class = 'WCS_Report_' . str_replace( '-', '_', $name ); + + if ( ! class_exists( $class ) ) { + return; + } + + $report = new $class(); + $report->output_report(); + } + + /** + * If we hit one of our reports in the WC get_report function, change the path to our dir. + * + * @param string $report_path the parth to the report. + * @param string $name the name of the report. + * @param string $class the class of the report. + * + * @return string path to the report template. + * @since 2.1 + * @deprecated in favor of autoloading + * @access private + */ + public static function initialize_reports_path( $report_path, $name, $class ) { + _deprecated_function( __METHOD__, '2.4.0' ); + if ( in_array( strtolower( $class ), array( + 'wc_report_subscription_events_by_date', + 'wc_report_upcoming_recurring_revenue', + 'wc_report_retention_rate', + 'wc_report_subscription_by_product', + 'wc_report_subscription_by_customer', + 'wc_report_subscription_payment_retry', + ) ) ) { + $report_path = dirname( __FILE__ ) . '/reports/classwcsreport' . $name . '.php'; + } + + return $report_path; + } +} diff --git a/includes/admin/class-wcs-admin-system-status.php b/includes/admin/class-wcs-admin-system-status.php old mode 100755 new mode 100644 index c23e03b..c866533 --- a/includes/admin/class-wcs-admin-system-status.php +++ b/includes/admin/class-wcs-admin-system-status.php @@ -37,6 +37,7 @@ class WCS_Admin_System_Status { self::set_debug_mode( $subscriptions_data ); self::set_staging_mode( $subscriptions_data ); + self::set_live_site_url( $subscriptions_data ); self::set_theme_overrides( $subscriptions_data ); self::set_subscription_statuses( $subscriptions_data ); self::set_woocommerce_account_data( $subscriptions_data ); @@ -91,23 +92,38 @@ class WCS_Admin_System_Status { $debug_data['wcs_debug'] = array( 'name' => _x( 'WCS_DEBUG', 'label that indicates whether debugging is turned on for the plugin', 'woocommerce-subscriptions' ), 'label' => 'WCS_DEBUG', - 'note' => ( $is_wcs_debug ) ? __( 'Yes', 'woocommerce-subscriptions' ) : __( 'No', 'woocommerce-subscriptions' ), + 'note' => ( $is_wcs_debug ) ? __( 'Yes', 'woocommerce-subscriptions' ) : __( 'No', 'woocommerce-subscriptions' ), 'success' => $is_wcs_debug ? 0 : 1, ); } /** * Include the staging/live mode the store is running in. + * + * @param array $debug_data */ private static function set_staging_mode( &$debug_data ) { $debug_data['wcs_staging'] = array( 'name' => _x( 'Subscriptions Mode', 'Live or Staging, Label on WooCommerce -> System Status page', 'woocommerce-subscriptions' ), 'label' => 'Subscriptions Mode', - 'note' => '' . ( ( WC_Subscriptions::is_duplicate_site() ) ? _x( 'Staging', 'refers to staging site', 'woocommerce-subscriptions' ) : _x( 'Live', 'refers to live site', 'woocommerce-subscriptions' ) ) . '', + 'note' => '' . ( ( WC_Subscriptions::is_duplicate_site() ) ? _x( 'Staging', 'refers to staging site', 'woocommerce-subscriptions' ) : _x( 'Live', 'refers to live site', 'woocommerce-subscriptions' ) ) . '', 'success' => ( WC_Subscriptions::is_duplicate_site() ) ? 0 : 1, ); } + /** + * @param array $debug_data + */ + private static function set_live_site_url( &$debug_data ) { + $debug_data['wcs_live_site_url'] = array( + 'name' => _x( 'Subscriptions Live URL', 'Live URL, Label on WooCommerce -> System Status page', 'woocommerce-subscriptions' ), + 'label' => 'Subscriptions Live URL', + 'note' => '' . esc_html( WC_Subscriptions::get_site_url_from_source( 'subscriptions_install' ) ) . '', + 'mark' => '', + 'mark_icon' => '', + ); + } + /** * List any Subscriptions template overrides. */ diff --git a/includes/admin/debug-tools/class-wcs-debug-tool-cache-background-updater.php b/includes/admin/debug-tools/class-wcs-debug-tool-cache-background-updater.php old mode 100755 new mode 100644 diff --git a/includes/admin/debug-tools/class-wcs-debug-tool-cache-eraser.php b/includes/admin/debug-tools/class-wcs-debug-tool-cache-eraser.php old mode 100755 new mode 100644 diff --git a/includes/admin/debug-tools/class-wcs-debug-tool-cache-generator.php b/includes/admin/debug-tools/class-wcs-debug-tool-cache-generator.php old mode 100755 new mode 100644 diff --git a/includes/admin/debug-tools/class-wcs-debug-tool-factory.php b/includes/admin/debug-tools/class-wcs-debug-tool-factory.php old mode 100755 new mode 100644 index 12377a2..b9bdd65 --- a/includes/admin/debug-tools/class-wcs-debug-tool-factory.php +++ b/includes/admin/debug-tools/class-wcs-debug-tool-factory.php @@ -30,14 +30,13 @@ final class WCS_Debug_Tool_Factory { * @param string $tool_name The section name given to the tool on the admin screen. * @param string $tool_desc The long description for the tool on the admin screen. * @param WCS_Cache_Updater $data_store + * @throws InvalidArgumentException When a class for the given tool is not found. */ public static function add_cache_tool( $tool_type, $tool_name, $tool_desc, WCS_Cache_Updater $data_store ) { - if ( ! is_admin() && ! defined( 'DOING_CRON' ) && ! defined( 'WP_CLI' ) ) { return; } - self::load_cache_tool_file( $tool_type ); $tool_class_name = self::get_cache_tool_class_name( $tool_type ); $tool_key = self::get_tool_key( $tool_name ); if ( 'generator' === $tool_type ) { @@ -46,6 +45,8 @@ final class WCS_Debug_Tool_Factory { } else { $tool = new $tool_class_name( $tool_key, $tool_name, $tool_desc, $data_store ); } + + /** @var WCS_Debug_Tool $tool */ $tool->init(); } @@ -66,49 +67,20 @@ final class WCS_Debug_Tool_Factory { * * To make sure the class's file is loaded, call @see self::load_cache_tool_class() first. * - * @param array $cache_tool_type The type of cache tool. Known tools are 'eraser' and 'generator'. + * @param string $cache_tool_type The type of cache tool. Known tools are 'eraser' and 'generator'. * @return string The cache tool's class name. */ protected static function get_cache_tool_class_name( $cache_tool_type ) { $tool_class_name = sprintf( 'WCS_Debug_Tool_Cache_%s', ucfirst( $cache_tool_type ) ); if ( ! class_exists( $tool_class_name ) ) { - throw new InvalidArgumentException( sprintf( '%s() requires a path to load %s. Class does not exist after loading %s.', __METHOD__, $class_name, $file_path ) ); + throw new InvalidArgumentException( sprintf( + '%s() requires a valid tool name. Class "%s" does not exist.', + __METHOD__, + $tool_class_name + ) ); } return $tool_class_name; } - - /** - * Load a cache tool file in the default file path. - * - * For example, load_cache_tool( 'related-order', 'generator' ) will load the file includes/admin/debug-tools/class-wcs-debug-tool-related-order-cache-generator.php - * - * @param array $cache_tool_type The type of cache tool. Known tools are 'eraser' and 'generator'. - */ - protected static function load_cache_tool_file( $cache_tool_type ) { - $file_path = sprintf( 'includes/admin/debug-tools/class-wcs-debug-tool-cache-%s.php', $cache_tool_type ); - $file_path = plugin_dir_path( WC_Subscriptions::$plugin_file ) . $file_path; - - if ( ! file_exists( $file_path ) ) { - throw new InvalidArgumentException( sprintf( '%s() requires a cache name linked to a valid debug tool. File does not exist: %s', __METHOD__, $rel_file_path ) ); - } - - self::load_required_classes( $cache_tool_type ); - require_once( $file_path ); - } - - /** - * Load classes that debug tools extend. - * - * @param array $cache_tool_type The type of cache tool. Known tools are 'eraser' and 'generator'. - */ - protected static function load_required_classes( $cache_tool_type ) { - - require_once( plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'includes/abstracts/abstract-wcs-debug-tool-cache-updater.php' ); - - if ( 'generator' === $cache_tool_type ) { - require_once( plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'includes/admin/debug-tools/class-wcs-debug-tool-cache-background-updater.php' ); - } - } } diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-payment-retries.php b/includes/admin/meta-boxes/class-wcs-meta-box-payment-retries.php old mode 100755 new mode 100644 index d7ab577..25e0ca1 --- a/includes/admin/meta-boxes/class-wcs-meta-box-payment-retries.php +++ b/includes/admin/meta-boxes/class-wcs-meta-box-payment-retries.php @@ -28,7 +28,7 @@ class WCS_Meta_Box_Payment_Retries { $retries = WCS_Retry_Manager::store()->get_retries_for_order( $post->ID ); - include_once( 'views/html-retries-table.php' ); + include_once( dirname( __FILE__ ) . '/views/html-retries-table.php' ); do_action( 'woocommerce_subscriptions_retries_meta_box', $post->ID, $retries ); } diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php b/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php old mode 100755 new mode 100644 index d8e9270..2edd545 --- a/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php +++ b/includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php @@ -33,7 +33,7 @@ class WCS_Meta_Box_Related_Orders { add_action( 'woocommerce_subscriptions_related_orders_meta_box_rows', __CLASS__ . '::output_rows', 10 ); - include_once( 'views/html-related-orders-table.php' ); + include_once( dirname( __FILE__ ) . '/views/html-related-orders-table.php' ); do_action( 'woocommerce_subscriptions_related_orders_meta_box', $order, $post ); } @@ -115,7 +115,7 @@ class WCS_Meta_Box_Related_Orders { if ( wcs_get_objects_property( $order, 'id' ) == $post->ID ) { continue; } - include( 'views/html-related-orders-row.php' ); + include( dirname( __FILE__ ) . '/views/html-related-orders-row.php' ); } } } diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-schedule.php b/includes/admin/meta-boxes/class-wcs-meta-box-schedule.php old mode 100755 new mode 100644 similarity index 96% rename from includes/admin/meta-boxes/class-wcs-meta-box-subscription-schedule.php rename to includes/admin/meta-boxes/class-wcs-meta-box-schedule.php index 7c2ab41..2c5c255 --- a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-schedule.php +++ b/includes/admin/meta-boxes/class-wcs-meta-box-schedule.php @@ -27,7 +27,7 @@ class WCS_Meta_Box_Schedule { $the_subscription = wcs_get_subscription( $post->ID ); } - include( 'views/html-subscription-schedule.php' ); + include( dirname( __FILE__ ) . '/views/html-subscription-schedule.php' ); } /** diff --git a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php b/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php old mode 100755 new mode 100644 index 00d1d6c..79e5f96 --- a/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php +++ b/includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php @@ -358,9 +358,17 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data { wcs_add_admin_notice( sprintf( __( 'Error updating some information: %s', 'woocommerce-subscriptions' ), $e->getMessage() ), 'error' ); } - // Grant download permissions on initial save. if ( isset( $_POST['original_post_status'] ) && 'auto-draft' === $_POST['original_post_status'] ) { - wc_downloadable_product_permissions( $post_id ); + $subscription->set_created_via( 'admin' ); + $subscription->save(); + + /** + * Fire an action after a subscription is created via the admin screen. + * + * @since 2.4.1 + * @param WC_Subscription $subscription The subscription object. + */ + do_action( 'woocommerce_admin_created_subscription', $subscription ); } do_action( 'woocommerce_process_shop_subscription_meta', $post_id, $post ); diff --git a/includes/admin/meta-boxes/views/html-related-orders-row.php b/includes/admin/meta-boxes/views/html-related-orders-row.php old mode 100755 new mode 100644 diff --git a/includes/admin/meta-boxes/views/html-related-orders-table.php b/includes/admin/meta-boxes/views/html-related-orders-table.php old mode 100755 new mode 100644 diff --git a/includes/admin/meta-boxes/views/html-retries-table.php b/includes/admin/meta-boxes/views/html-retries-table.php old mode 100755 new mode 100644 diff --git a/includes/admin/meta-boxes/views/html-subscription-schedule.php b/includes/admin/meta-boxes/views/html-subscription-schedule.php old mode 100755 new mode 100644 diff --git a/includes/admin/reports/class-wcs-report-cache-manager.php b/includes/admin/reports/class-wcs-report-cache-manager.php old mode 100755 new mode 100644 index 97d071e..eb40b48 --- a/includes/admin/reports/class-wcs-report-cache-manager.php +++ b/includes/admin/reports/class-wcs-report-cache-manager.php @@ -27,30 +27,30 @@ class WCS_Report_Cache_Manager { */ private $update_events_and_classes = array( 'woocommerce_subscriptions_reports_schedule_cache_updates' => array( // a custom hook that can be called to schedule a full cache update, used by WC_Subscriptions_Upgrader - 0 => 'WC_Report_Subscription_Events_By_Date', - 1 => 'WC_Report_Upcoming_Recurring_Revenue', - 3 => 'WC_Report_Subscription_By_Product', - 4 => 'WC_Report_Subscription_By_Customer', + 0 => 'WCS_Report_Subscription_Events_By_Date', + 1 => 'WCS_Report_Upcoming_Recurring_Revenue', + 3 => 'WCS_Report_Subscription_By_Product', + 4 => 'WCS_Report_Subscription_By_Customer', ), 'woocommerce_subscription_payment_complete' => array( // this hook takes care of renewal, switch and initial payments - 0 => 'WC_Report_Subscription_Events_By_Date', - 4 => 'WC_Report_Subscription_By_Customer', + 0 => 'WCS_Report_Subscription_Events_By_Date', + 4 => 'WCS_Report_Subscription_By_Customer', ), 'woocommerce_subscriptions_switch_completed' => array( - 0 => 'WC_Report_Subscription_Events_By_Date', + 0 => 'WCS_Report_Subscription_Events_By_Date', ), 'woocommerce_subscription_status_changed' => array( - 0 => 'WC_Report_Subscription_Events_By_Date', // we really only need cancelled, expired and active status here, but we'll use a more generic hook for convenience - 4 => 'WC_Report_Subscription_By_Customer', + 0 => 'WCS_Report_Subscription_Events_By_Date', // we really only need cancelled, expired and active status here, but we'll use a more generic hook for convenience + 4 => 'WCS_Report_Subscription_By_Customer', ), 'woocommerce_subscription_status_active' => array( - 1 => 'WC_Report_Upcoming_Recurring_Revenue', + 1 => 'WCS_Report_Upcoming_Recurring_Revenue', ), 'woocommerce_new_order_item' => array( - 3 => 'WC_Report_Subscription_By_Product', + 3 => 'WCS_Report_Subscription_By_Product', ), 'woocommerce_update_order_item' => array( - 3 => 'WC_Report_Subscription_By_Product', + 3 => 'WCS_Report_Subscription_By_Product', ), ); @@ -141,15 +141,7 @@ class WCS_Report_Cache_Manager { // On large sites, we want to run the cache update once at 4am in the site's timezone if ( $this->use_large_site_cache() ) { - $four_am_site_time = new WC_DateTime( '4 am', wcs_get_sites_timezone() ); - - // Convert to a UTC timestamp for scheduling - $cache_update_timestamp = $four_am_site_time->getTimestamp(); - - // PHP doesn't support a "next 4am" time format equivalent, so we need to manually handle getting 4am from earlier today (which will always happen when this is run after 4am and before midnight in the site's timezone) - if ( $cache_update_timestamp <= gmdate( 'U' ) ) { - $cache_update_timestamp += DAY_IN_SECONDS; - } + $cache_update_timestamp = $this->get_large_site_cache_update_timestamp(); // Schedule one update event for each class to avoid updating cache more than once for the same class for different events foreach ( $this->reports_to_update as $index => $report_class ) { @@ -217,11 +209,6 @@ class WCS_Report_Cache_Manager { require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' ); require_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); - $report_name = strtolower( str_replace( '_', '-', str_replace( 'WC_Report_', '', $report_class ) ) ); - $report_path = WCS_Admin_Reports::initialize_reports_path( '', $report_name, $report_class ); - - require_once( $report_path ); - $reflector = new ReflectionMethod( $report_class, 'get_data' ); // Some report classes extend WP_List_Table which has a constructor using methods not available on WP-Cron (and unable to be loaded with a __doing_it_wrong() notice), so they have a static get_data() method and do not need to be instantiated @@ -354,6 +341,21 @@ class WCS_Report_Cache_Manager { return $data; } -} -return new WCS_Report_Cache_Manager(); + /** + * Get the scheduled update cache time for large sites. + * + * @return int The timestamp of the next occurring 4 am in the site's timezone converted to UTC. + */ + protected function get_large_site_cache_update_timestamp() { + // Get the timestamp for 4 am in the site's timezone converted to the UTC equivalent. + $cache_update_timestamp = wc_string_to_timestamp( '4 am', current_time( 'timestamp' ) ) - wc_timezone_offset(); + + // PHP doesn't support a "next 4am" time format equivalent, so we need to manually handle getting 4am from earlier today (which will always happen when this is run after 4am and before midnight in the site's timezone) + if ( $cache_update_timestamp <= gmdate( 'U' ) ) { + $cache_update_timestamp += DAY_IN_SECONDS; + } + + return $cache_update_timestamp; + } +} diff --git a/includes/admin/reports/class-wcs-report-dashboard.php b/includes/admin/reports/class-wcs-report-dashboard.php old mode 100755 new mode 100644 index 2f004a7..748d869 --- a/includes/admin/reports/class-wcs-report-dashboard.php +++ b/includes/admin/reports/class-wcs-report-dashboard.php @@ -96,5 +96,3 @@ class WCS_Report_Dashboard { wp_enqueue_style( 'wcs-dashboard-report', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/css/dashboard.css', array(), WC_Subscriptions::$version ); } } - -return new WCS_Report_Dashboard(); diff --git a/includes/admin/reports/class-wcs-report-retention-rate.php b/includes/admin/reports/class-wcs-report-retention-rate.php old mode 100755 new mode 100644 index 0ecfa1e..d7f1bca --- a/includes/admin/reports/class-wcs-report-retention-rate.php +++ b/includes/admin/reports/class-wcs-report-retention-rate.php @@ -5,13 +5,13 @@ * Find the number of periods between when each subscription is created and ends or ended * then plot all subscriptions using this data to provide a curve of retention rates. * - * @package WooCommerce Subscriptions - * @subpackage WC_Subscriptions_Admin_Reports - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WC_Subscriptions_Admin_Reports + * @category Class + * @author Prospress + * @since 2.1 */ -class WC_Report_Retention_Rate extends WC_Admin_Report { +class WCS_Report_Retention_Rate extends WC_Admin_Report { public $chart_colours = array(); diff --git a/includes/admin/reports/class-wcs-report-subscription-by-customer.php b/includes/admin/reports/class-wcs-report-subscription-by-customer.php old mode 100755 new mode 100644 index 079bf0e..5fa1868 --- a/includes/admin/reports/class-wcs-report-subscription-by-customer.php +++ b/includes/admin/reports/class-wcs-report-subscription-by-customer.php @@ -4,13 +4,13 @@ * * Creates the subscription admin reports area. * - * @package WooCommerce Subscriptions - * @subpackage WC_Subscriptions_Admin_Reports - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WC_Subscriptions_Admin_Reports + * @category Class + * @author Prospress + * @since 2.1 */ -class WC_Report_Subscription_By_Customer extends WP_List_Table { +class WCS_Report_Subscription_By_Customer extends WP_List_Table { private $totals; diff --git a/includes/admin/reports/class-wcs-report-subscription-by-product.php b/includes/admin/reports/class-wcs-report-subscription-by-product.php old mode 100755 new mode 100644 index 9b53c50..2fa4f62 --- a/includes/admin/reports/class-wcs-report-subscription-by-product.php +++ b/includes/admin/reports/class-wcs-report-subscription-by-product.php @@ -4,13 +4,13 @@ * * Creates the subscription admin reports area. * - * @package WooCommerce Subscriptions - * @subpackage WC_Subscriptions_Admin_Reports - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WC_Subscriptions_Admin_Reports + * @category Class + * @author Prospress + * @since 2.1 */ -class WC_Report_Subscription_By_Product extends WP_List_Table { +class WCS_Report_Subscription_By_Product extends WP_List_Table { /** * Constructor. diff --git a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php old mode 100755 new mode 100644 index 541908d..d394bc3 --- a/includes/admin/reports/class-wcs-report-subscription-events-by-date.php +++ b/includes/admin/reports/class-wcs-report-subscription-events-by-date.php @@ -4,13 +4,13 @@ * * Display important historical data for subscription revenue and events, like switches and cancellations. * - * @package WooCommerce Subscriptions - * @subpackage WC_Subscriptions_Admin_Reports - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WC_Subscriptions_Admin_Reports + * @category Class + * @author Prospress + * @since 2.1 */ -class WC_Report_Subscription_Events_By_Date extends WC_Admin_Report { +class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report { public $chart_colours = array(); @@ -257,7 +257,7 @@ class WC_Report_Subscription_Events_By_Date extends WC_Admin_Report { $query = $wpdb->prepare( "SELECT searchdate.Date as date, COUNT( DISTINCT wcsubs.ID) as count FROM ( - SELECT DATE_FORMAT(last_thousand_days.Date,'%%Y-%%m-%%d') as Date + SELECT DATE(last_thousand_days.Date) as Date FROM ( SELECT DATE(%s) - INTERVAL(units.digit + (10 * tens.digit) + (100 * hundreds.digit)) DAY as Date FROM ( @@ -295,6 +295,7 @@ class WC_Report_Subscription_Events_By_Date extends WC_Admin_Report { OR wcsmeta.meta_value = 0 OR wcsmeta.meta_value IS NULL ) + GROUP BY searchdate.Date ORDER BY searchdate.Date ASC", $query_end_date, date( 'Y-m-d', $this->start_date ), @@ -380,7 +381,7 @@ class WC_Report_Subscription_Events_By_Date extends WC_Admin_Report { $this->report_data->switch_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->switch_counts, 'count' ) ) ); $this->report_data->total_subscriptions_cancelled = absint( array_sum( wp_list_pluck( $this->report_data->cancel_counts, 'count' ) ) ); $this->report_data->total_subscriptions_ended = absint( array_sum( wp_list_pluck( $this->report_data->ended_counts, 'count' ) ) ); - $this->report_data->total_subscriptions_at_period_end = absint( end( $this->report_data->subscriber_counts )->count ); + $this->report_data->total_subscriptions_at_period_end = $this->report_data->subscriber_counts ? absint( end( $this->report_data->subscriber_counts )->count ) : 0; $this->report_data->total_subscriptions_at_period_start = isset( $this->report_data->subscriber_counts[0]->count ) ? absint( $this->report_data->subscriber_counts[0]->count ) : 0; } diff --git a/includes/admin/reports/class-wcs-report-subscription-payment-retry.php b/includes/admin/reports/class-wcs-report-subscription-payment-retry.php old mode 100755 new mode 100644 index 77d7574..235b6c9 --- a/includes/admin/reports/class-wcs-report-subscription-payment-retry.php +++ b/includes/admin/reports/class-wcs-report-subscription-payment-retry.php @@ -4,13 +4,13 @@ * * Creates the subscription admin reports area. * - * @package WooCommerce Subscriptions - * @subpackage WC_Subscriptions_Admin_Reports - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WC_Subscriptions_Admin_Reports + * @category Class + * @author Prospress + * @since 2.1 */ -class WC_Report_Subscription_Payment_Retry extends WC_Admin_Report { +class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report { private $chart_colours = array(); diff --git a/includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php b/includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php old mode 100755 new mode 100644 index 89524f0..c438d37 --- a/includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php +++ b/includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php @@ -5,13 +5,13 @@ * Display the renewal order count and revenue that will be processed for all currently active subscriptions * for a given period of time in the future. * - * @package WooCommerce Subscriptions - * @subpackage WC_Subscriptions_Admin_Reports - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WC_Subscriptions_Admin_Reports + * @category Class + * @author Prospress + * @since 2.1 */ -class WC_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report { +class WCS_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report { public $chart_colours = array(); @@ -119,7 +119,7 @@ class WC_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report { // Query based on whole days, not minutes/hours so that we can cache the query for at least 24 hours $base_query = $wpdb->prepare( "SELECT - DATE_FORMAT(ms.meta_value, '%s') as scheduled_date, + DATE(ms.meta_value) as scheduled_date, SUM(mo.meta_value) as recurring_total, COUNT(mo.meta_value) as total_renewals, group_concat(p.ID) as subscription_ids, @@ -148,7 +148,6 @@ class WC_Report_Upcoming_Recurring_Revenue extends WC_Admin_Report { AND me.meta_key = '_schedule_end ' GROUP BY {$this->group_by_query} ORDER BY ms.meta_value ASC", - '%Y-%m-%d', date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ), date( 'Y-m-d', $this->start_date ), date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ) diff --git a/includes/admin/reports/deprecated/class-wc-report-retention-rate.php b/includes/admin/reports/deprecated/class-wc-report-retention-rate.php new file mode 100644 index 0000000..e222fd0 --- /dev/null +++ b/includes/admin/reports/deprecated/class-wc-report-retention-rate.php @@ -0,0 +1,20 @@ +data['resubscribed_subscription'] = strval( reset( $resubscribed_subscriptions ) ); // Subscriptions can only be resubscribed to once so return the first and only element. foreach ( array( 'start', 'trial_end', 'next_payment', 'end' ) as $date_type ) { - $date_type_key = ( 'start' === $date_type ) ? 'date_created' : $date_type; - $date = $subscription->get_date( $date_type_key ); - + $date = $subscription->get_date( $date_type ); $response->data[ $date_type . '_date' ] = ( ! empty( $date ) ) ? wc_rest_prepare_date_response( $date ) : ''; } @@ -296,6 +294,10 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller { $payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription ); + // Reload the subscription to update the meta values. + // In particular, the update_post_meta() called while _stripe_card_id is updated to _stripe_source_id + $subscription = wcs_get_subscription( $subscription->get_id() ); + if ( isset( $payment_method_meta[ $payment_method ] ) ) { $payment_method_meta = $payment_method_meta[ $payment_method ]; @@ -318,6 +320,9 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller { $subscription->set_payment_method( $payment_method, $payment_method_meta ); + // Save the subscription to reflect the new values + $subscription->save(); + } catch ( Exception $e ) { $subscription->set_payment_method(); $subscription->save(); @@ -346,14 +351,14 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller { if ( ! is_null( $value ) ) { switch ( $key ) { - case 'billing' : - case 'shipping' : + case 'billing': + case 'shipping': $this->update_address( $subscription, $value, $key ); break; - case 'line_items' : - case 'shipping_lines' : - case 'fee_lines' : - case 'coupon_lines' : + case 'line_items': + case 'shipping_lines': + case 'fee_lines': + case 'coupon_lines': if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( is_array( $item ) ) { @@ -366,14 +371,13 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller { } } break; - case 'start_date' : - case 'trial_end_date' : - case 'next_payment_date' : - case 'end_date' : - $date_type_key = ( 'start_date' === $key ) ? 'date_created' : $key; - $dates_to_update[ $date_type_key ] = $value; + case 'start_date': + case 'trial_end_date': + case 'next_payment_date': + case 'end_date': + $dates_to_update[ $key ] = $value; break; - default : + default: if ( is_callable( array( $subscription, "set_{$key}" ) ) ) { $subscription->{"set_{$key}"}( $value ); } diff --git a/includes/api/legacy/class-wc-api-subscriptions-customers.php b/includes/api/legacy/class-wc-api-subscriptions-customers.php old mode 100755 new mode 100644 diff --git a/includes/api/legacy/class-wc-api-subscriptions.php b/includes/api/legacy/class-wc-api-subscriptions.php old mode 100755 new mode 100644 index 057e1a3..423901a --- a/includes/api/legacy/class-wc-api-subscriptions.php +++ b/includes/api/legacy/class-wc-api-subscriptions.php @@ -397,10 +397,8 @@ class WC_API_Subscriptions extends WC_API_Orders { $dates_to_update = array(); foreach ( array( 'start', 'trial_end', 'end', 'next_payment' ) as $date_type ) { - if ( isset( $data[ $date_type . '_date' ] ) ) { - $date_type_key = ( 'start' === $date_type ) ? 'date_created' : $date_type; - $dates_to_update[ $date_type_key ] = $data[ $date_type . '_date' ]; + $dates_to_update[ $date_type ] = $data[ $date_type . '_date' ]; } } @@ -461,7 +459,7 @@ class WC_API_Subscriptions extends WC_API_Orders { $subscription_data['billing_schedule'] = array( 'period' => $subscription->get_billing_period(), 'interval' => $subscription->get_billing_interval(), - 'start_at' => $this->get_formatted_datetime( $subscription, 'date_created' ), + 'start_at' => $this->get_formatted_datetime( $subscription, 'start' ), 'trial_end_at' => $this->get_formatted_datetime( $subscription, 'trial_end' ), 'next_payment_at' => $this->get_formatted_datetime( $subscription, 'next_payment' ), 'end_at' => $this->get_formatted_datetime( $subscription, 'end' ), diff --git a/includes/api/legacy/class-wc-rest-subscription-notes-controller.php b/includes/api/legacy/class-wc-rest-subscription-notes-controller.php old mode 100755 new mode 100644 diff --git a/includes/api/legacy/class-wc-rest-subscriptions-controller.php b/includes/api/legacy/class-wc-rest-subscriptions-controller.php old mode 100755 new mode 100644 index 4ea4b0d..acb3eef --- a/includes/api/legacy/class-wc-rest-subscriptions-controller.php +++ b/includes/api/legacy/class-wc-rest-subscriptions-controller.php @@ -85,7 +85,7 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_Controller { $response->data['billing_period'] = $subscription->get_billing_period(); $response->data['billing_interval'] = $subscription->get_billing_interval(); - $response->data['start_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'date_created' ) ); + $response->data['start_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'start_date' ) ); $response->data['trial_end_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'trial_end' ) ); $response->data['next_payment_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'next_payment' ) ); $response->data['end_date'] = wc_rest_prepare_date_response( $subscription->get_date( 'end_date' ) ); diff --git a/includes/class-wc-order-item-pending-switch.php b/includes/class-wc-order-item-pending-switch.php old mode 100755 new mode 100644 diff --git a/includes/class-wc-product-subscription-variation.php b/includes/class-wc-product-subscription-variation.php old mode 100755 new mode 100644 diff --git a/includes/class-wc-product-subscription.php b/includes/class-wc-product-subscription.php old mode 100755 new mode 100644 diff --git a/includes/class-wc-product-variable-subscription.php b/includes/class-wc-product-variable-subscription.php old mode 100755 new mode 100644 index 32b253d..f1e2ef2 --- a/includes/class-wc-product-variable-subscription.php +++ b/includes/class-wc-product-variable-subscription.php @@ -312,4 +312,23 @@ class WC_Product_Variable_Subscription extends WC_Product_Variable { return $prefix; } + + /** + * Get an array of available variations for the current product. + * + * @return array + */ + public function get_available_variations() { + $available_variations_data = parent::get_available_variations(); + + foreach ( $available_variations_data as $index => $variation_data ) { + + // Add the product's synced first payment date to the variation data if applicable. + if ( isset( $variation_data['variation_id'] ) ) { + $available_variations_data[ $index ]['first_payment_html'] = WC_Subscriptions_Synchroniser::get_products_first_payment_date( wc_get_product( $variation_data['variation_id'] ) ); + } + } + + return $available_variations_data; + } } diff --git a/includes/class-wc-subscription.php b/includes/class-wc-subscription.php old mode 100755 new mode 100644 index 364cc21..c5c778c --- a/includes/class-wc-subscription.php +++ b/includes/class-wc-subscription.php @@ -67,6 +67,7 @@ class WC_Subscription extends WC_Order { 'schedule_cancelled' => null, 'schedule_end' => null, 'schedule_payment_retry' => null, + 'schedule_start' => null, 'switch_data' => array(), ); @@ -98,8 +99,22 @@ class WC_Subscription extends WC_Order { */ public function __construct( $subscription ) { - parent::__construct( $subscription ); + // Add subscription date types as extra subscription data. + foreach ( wcs_get_subscription_date_types() as $date_type => $date_name ) { + // The last payment date is derived from other sources and shouldn't be stored on a subscription. + if ( 'last_payment' === $date_type ) { + continue; + } + $date_type_key = wcs_maybe_prefix_key( $date_type, 'schedule_' ); + + // Skip any custom dates which are already core date types. + if ( ! isset( $this->extra_data[ $date_type_key ] ) ) { + $this->extra_data[ $date_type_key ] = null; + } + } + + parent::__construct( $subscription ); $this->order_type = 'shop_subscription'; } @@ -1147,7 +1162,7 @@ class WC_Subscription extends WC_Order { // Delete dates with a 0 date time if ( 0 == $datetime ) { - if ( ! in_array( $date_type, array( 'date_created', 'last_order_date_created', 'last_order_date_modified' ) ) ) { + if ( ! in_array( $date_type, array( 'date_created', 'start', 'last_order_date_created', 'last_order_date_modified' ) ) ) { $this->delete_date( $date_type ); } continue; @@ -1210,6 +1225,9 @@ class WC_Subscription extends WC_Order { // Make sure some dates are before next payment date switch ( $date_type ) { case 'date_created' : + $message = __( 'The creation date of a subscription can not be deleted, only updated.', 'woocommerce-subscriptions' ); + break; + case 'start' : $message = __( 'The start date of a subscription can not be deleted, only updated.', 'woocommerce-subscriptions' ); break; case 'last_order_date_created' : @@ -1241,6 +1259,7 @@ class WC_Subscription extends WC_Order { public function can_date_be_updated( $date_type ) { switch ( $date_type ) { + case 'start': case 'date_created' : if ( $this->has_status( array( 'auto-draft', 'pending' ) ) ) { $can_date_be_updated = true; @@ -1330,7 +1349,7 @@ class WC_Subscription extends WC_Order { $next_payment_date = 0; // If the subscription is not active, there is no next payment date - $start_time = $this->get_time( 'date_created' ); + $start_time = $this->get_time( 'start' ); $next_payment_time = $this->get_time( 'next_payment' ); $trial_end_time = $this->get_time( 'trial_end' ); $last_payment_time = max( $this->get_time( 'last_order_date_created' ), $this->get_time( 'last_order_date_paid' ) ); @@ -1754,8 +1773,8 @@ class WC_Subscription extends WC_Order { * @return array */ public function get_related_orders_query( $subscription_id ) { - wcs_deprecated_function( __METHOD__, '2.3.0', 'WCS_Subscription_Data_Store_CPT::get_related_order_ids( $subscription_id ) via WCS_Subscription_Data_Store::instance()' ); - return WCS_Related_Order_Store::instance()->get_related_order_ids( $subscription_id, 'renewal' ); + wcs_deprecated_function( __METHOD__, '2.3.0', 'WCS_Subscription_Data_Store_CPT::get_related_order_ids( wcs_get_subscription( $subscription_id ) ) via WCS_Subscription_Data_Store::instance()' ); + return WCS_Related_Order_Store::instance()->get_related_order_ids( wcs_get_subscription( $subscription_id ), 'renewal' ); } /** @@ -1959,28 +1978,7 @@ class WC_Subscription extends WC_Order { do_action( 'woocommerce_subscription_validate_payment_meta', $payment_method_id, $payment_meta, $this ); do_action( 'woocommerce_subscription_validate_payment_meta_' . $payment_method_id, $payment_meta, $this ); - foreach ( $payment_meta as $meta_table => $meta ) { - foreach ( $meta as $meta_key => $meta_data ) { - if ( isset( $meta_data['value'] ) ) { - switch ( $meta_table ) { - case 'user_meta': - case 'usermeta': - update_user_meta( $this->get_user_id(), $meta_key, $meta_data['value'] ); - break; - case 'post_meta': - case 'postmeta': - $this->update_meta_data( $meta_key, $meta_data['value'] ); - break; - case 'options': - update_option( $meta_key, $meta_data['value'] ); - break; - default: - do_action( 'wcs_save_other_payment_meta', $this, $meta_table, $meta_key, $meta_data['value'] ); - } - } - } - } - + wcs_set_payment_meta( $this, $payment_meta ); } /** @@ -2098,6 +2096,13 @@ class WC_Subscription extends WC_Order { $sign_up_fee = ( (float) $original_order_item->get_total( 'edit' ) ) / $original_order_item->get_quantity( 'edit' ); } elseif ( $original_order_item->meta_exists( '_synced_sign_up_fee' ) ) { $sign_up_fee = ( (float) $original_order_item->get_meta( '_synced_sign_up_fee' ) ) / $original_order_item->get_quantity( 'edit' ); + + // The synced sign up fee meta contains the raw product sign up fee, if the subscription totals are inclusive of tax, we need to adjust the synced sign up fee to match tax inclusivity. + if ( $this->get_prices_include_tax() ) { + $line_item_total = (float) $original_order_item->get_total( 'edit' ) + $original_order_item->get_total_tax( 'edit' ); + $signup_fee_portion = $sign_up_fee / $line_item_total; + $sign_up_fee = (float) $original_order_item->get_total( 'edit' ) * $signup_fee_portion; + } } else { // Sign-up fee is any amount on top of recurring amount $order_line_total = ( (float) $original_order_item->get_total( 'edit' ) ) / $original_order_item->get_quantity( 'edit' ); @@ -2106,17 +2111,13 @@ class WC_Subscription extends WC_Order { $sign_up_fee = max( $order_line_total - $subscription_line_total, 0 ); } - if ( ! empty( $original_order_item ) && ! empty( $sign_up_fee ) ) { + // If prices don't inc tax, ensure that the sign up fee amount includes the tax. + if ( 'inclusive_of_tax' === $tax_inclusive_or_exclusive && ! empty( $original_order_item ) && ! empty( $sign_up_fee ) ) { $sign_up_fee_proportion = $sign_up_fee / ( $original_order_item->get_total( 'edit' ) / $original_order_item->get_quantity( 'edit' ) ); - $sign_up_fee_tax = wc_round_tax_total( $original_order_item->get_total_tax( 'edit' ) * $sign_up_fee_proportion ); + $sign_up_fee_tax = $original_order_item->get_total_tax( 'edit' ) * $sign_up_fee_proportion; - // If prices don't inc tax, ensure that the sign up fee amount includes the tax. - if ( 'inclusive_of_tax' === $tax_inclusive_or_exclusive && ! $this->get_prices_include_tax() ) { - $sign_up_fee += $sign_up_fee_tax; - // If prices inc tax and the request is for prices exclusive of tax, remove the taxes. - } elseif ( 'inclusive_of_tax' !== $tax_inclusive_or_exclusive && $this->get_prices_include_tax() ) { - $sign_up_fee -= $sign_up_fee_tax; - } + $sign_up_fee += $sign_up_fee_tax; + $sign_up_fee = wc_format_decimal( $sign_up_fee, wc_get_price_decimals() ); } } @@ -2135,7 +2136,7 @@ class WC_Subscription extends WC_Order { if ( 0 != ( $end_time = $this->get_time( 'end' ) ) ) { - $from_timestamp = $this->get_time( 'date_created' ); + $from_timestamp = $this->get_time( 'start' ); if ( 0 != $this->get_time( 'trial_end' ) || WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $this ) ) { @@ -2218,8 +2219,8 @@ class WC_Subscription extends WC_Order { // Get a full set of subscription dates made up of passed and current dates foreach ( $this->get_valid_date_types() as $date_type ) { - // While 'start' & 'last_payment' are valid date types, they are deprecated and we use 'date_created' & 'last_order_date_created' to refer to them now instead - if ( in_array( $date_type, array( 'last_payment', 'start' ) ) ) { + // While 'last_payment' is a valid date type, it is deprecated and we use 'last_order_date_created' now instead + if ( 'last_payment' === $date_type ) { continue; } @@ -2288,7 +2289,7 @@ class WC_Subscription extends WC_Order { $messages[] = sprintf( __( 'The %s date must occur after the trial end date.', 'woocommerce-subscriptions' ), $date_type ); } case 'trial_end' : - if ( $timestamp <= $timestamps['date_created'] ) { + if ( ! in_array( $date_type, array( 'end', 'cancelled' ) ) && $timestamp <= $timestamps['start'] ) { $messages[] = sprintf( __( 'The %s date must occur after the start date.', 'woocommerce-subscriptions' ), $date_type ); } } @@ -2353,6 +2354,24 @@ class WC_Subscription extends WC_Order { return $this->valid_date_types; } + /** + * Get the subscription's payment method meta. + * + * @since 2.4.3 + * @return array The subscription's payment meta in the format returned by the woocommerce_subscription_payment_meta filter. + */ + public function get_payment_method_meta() { + WC()->payment_gateways(); + + if ( $this->is_manual() ) { + return array(); + } + + $payment_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $this ); + + return isset( $payment_meta[ $this->get_payment_method() ] ) ? $payment_meta[ $this->get_payment_method() ]: array(); + } + /************************ * WC_Order overrides * diff --git a/includes/class-wc-subscriptions-addresses.php b/includes/class-wc-subscriptions-addresses.php old mode 100755 new mode 100644 index 381e0cc..d03162f --- a/includes/class-wc-subscriptions-addresses.php +++ b/includes/class-wc-subscriptions-addresses.php @@ -28,6 +28,7 @@ class WC_Subscriptions_Addresses { add_filter( 'woocommerce_address_to_edit', __CLASS__ . '::maybe_populate_subscription_addresses', 10 ); + add_filter( 'woocommerce_get_breadcrumb', __CLASS__ . '::change_addresses_breadcrumb', 10, 1 ); } /** @@ -80,12 +81,15 @@ class WC_Subscriptions_Addresses { } // translators: $1: address type (Shipping Address / Billing Address), $2: opening tag, $3: closing tag - $label = sprintf( __( 'Update the %1$s used for %2$sall%3$s of my active subscriptions', 'woocommerce-subscriptions' ), wcs_get_address_type_to_display( $address_type ), '', '' ); + $label = sprintf( esc_html__( 'Update the %1$s used for %2$sall%3$s of my active subscriptions', 'woocommerce-subscriptions' ), wcs_get_address_type_to_display( $address_type ), '', '' ); - woocommerce_form_field( 'update_all_subscriptions_addresses', array( - 'type' => 'checkbox', - 'class' => array( 'form-row-wide' ), - 'label' => $label, + woocommerce_form_field( + 'update_all_subscriptions_addresses', + array( + 'type' => 'checkbox', + 'class' => array( 'form-row-wide' ), + 'label' => $label, + 'default' => apply_filters( 'wcs_update_all_subscriptions_addresses_checked', false ), ) ); } @@ -176,6 +180,39 @@ class WC_Subscriptions_Addresses { public static function maybe_update_order_address( $subscription, $address_fields ) { _deprecated_function( __METHOD__, '2.0', 'WC_Order::set_address() or WC_Subscription::set_address()' ); } -} -WC_Subscriptions_Addresses::init(); + /** + * Replace the change address breadcrumbs structure to include a link back to the subscription. + * + * @param array $crumbs + * @return array + * @since 2.4.2 + */ + public static function change_addresses_breadcrumb( $crumbs ) { + if ( isset( $_GET['subscription'] ) && is_wc_endpoint_url() && 'edit-address' === WC()->query->get_current_endpoint() ) { + global $wp_query; + $subscription = wcs_get_subscription( absint( $_GET['subscription'] ) ); + + if ( ! $subscription ) { + return $crumbs; + } + + $crumbs[1] = array( + get_the_title( wc_get_page_id( 'myaccount' ) ), + get_permalink( wc_get_page_id( 'myaccount' ) ), + ); + + $crumbs[2] = array( + sprintf( _x( 'Subscription #%s', 'hash before order number', 'woocommerce-subscriptions' ), $subscription->get_order_number() ), + esc_url( $subscription->get_view_order_url() ), + ); + + $crumbs[3] = array( + sprintf( _x( 'Change %s address', 'change billing or shipping address', 'woocommerce-subscriptions' ), $wp_query->query_vars['edit-address'] ), + '', + ); + } + + return $crumbs; + } +} diff --git a/includes/class-wc-subscriptions-cart.php b/includes/class-wc-subscriptions-cart.php old mode 100755 new mode 100644 index e9d3729..e1a3e82 --- a/includes/class-wc-subscriptions-cart.php +++ b/includes/class-wc-subscriptions-cart.php @@ -91,7 +91,7 @@ class WC_Subscriptions_Cart { add_action( 'woocommerce_cart_totals_after_order_total', __CLASS__ . '::display_recurring_totals' ); add_action( 'woocommerce_review_order_after_order_total', __CLASS__ . '::display_recurring_totals' ); - add_action( 'woocommerce_add_to_cart_validation', __CLASS__ . '::check_valid_add_to_cart', 10, 6 ); + add_filter( 'woocommerce_add_to_cart_validation', __CLASS__ . '::check_valid_add_to_cart', 10, 6 ); add_filter( 'woocommerce_cart_needs_shipping', __CLASS__ . '::cart_needs_shipping', 11, 1 ); @@ -349,7 +349,17 @@ class WC_Subscriptions_Cart { unset( WC()->session->wcs_shipping_methods ); // If there is no sign-up fee and a free trial, and no products being purchased with the subscription, we need to zero the fees for the first billing period - if ( 0 == self::get_cart_subscription_sign_up_fee() && self::all_cart_items_have_free_trial() ) { + $remove_fees_from_cart = ( 0 == self::get_cart_subscription_sign_up_fee() && self::all_cart_items_have_free_trial() ); + + /** + * Allow third-parties to override whether the fees will be removed from the initial order cart. + * + * @since 2.4.3 + * @param bool $remove_fees_from_cart Whether the fees will be removed. By default fees will be removed if there is no signup fee and all cart items have a trial. + * @param WC_Cart $cart The standard WC cart object. + * @param array $recurring_carts All the recurring cart objects. + */ + if ( apply_filters( 'wcs_remove_fees_from_initial_cart', $remove_fees_from_cart, $cart, $recurring_carts ) ) { $cart_fees = WC()->cart->get_fees(); if ( WC_Subscriptions::is_woocommerce_pre( '3.2' ) ) { @@ -2154,4 +2164,3 @@ class WC_Subscriptions_Cart { return $package_rates; } } -WC_Subscriptions_Cart::init(); diff --git a/includes/class-wc-subscriptions-change-payment-gateway.php b/includes/class-wc-subscriptions-change-payment-gateway.php old mode 100755 new mode 100644 index facede0..ff166e7 --- a/includes/class-wc-subscriptions-change-payment-gateway.php +++ b/includes/class-wc-subscriptions-change-payment-gateway.php @@ -62,6 +62,9 @@ class WC_Subscriptions_Change_Payment_Gateway { // Change the "Pay for Order" page title to "Change Payment Method" add_filter( 'the_title', __CLASS__ . '::change_payment_method_page_title', 100 ); + // Change the "Pay for Order" breadcrumb to "Change Payment Method" + add_filter( 'woocommerce_get_breadcrumb', __CLASS__ . '::change_payment_method_breadcrumb', 10, 1 ); + // Maybe filter subscriptions_needs_payment to return false when processing change-payment-gateway requests add_filter( 'woocommerce_subscription_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 1 ); } @@ -149,6 +152,18 @@ class WC_Subscriptions_Change_Payment_Gateway { $subscription_key = isset( $_GET['key'] ) ? wc_clean( $_GET['key'] ) : ''; $subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) ); + /** + * wcs_before_replace_pay_shortcode + * + * Action that allows payment methods to modify the subscription object so, for example, + * if the new payment method still hasn't been set, they can set it temporarily (without saving) + * + * @since 2.4.0 + * + * @param WC_Subscription $subscription + */ + do_action( 'wcs_before_replace_pay_shortcode', $subscription ); + if ( $subscription->get_id() == absint( $wp->query_vars['order-pay'] ) && $subscription->get_order_key() == $subscription_key ) { ?> @@ -438,9 +453,29 @@ class WC_Subscriptions_Change_Payment_Gateway { * @since 1.4 */ public static function get_available_payment_gateways( $available_gateways ) { + $is_change_payment_method_request = isset( $_GET['change_payment_method'] ); - // The customer change payment method flow uses the order pay endpoint and so we only need to check for order pay endpoints along side cart related conditions. - if ( isset( $_GET['change_payment_method'] ) || ( ! is_wc_endpoint_url( 'order-pay' ) && wcs_cart_contains_failed_renewal_order_payment() ) ) { + // If we're on a order-pay page but not changing a subscription's payment method, exit early - we don't want to filter the available payment gateways while the customer pays for an order. + if ( ! $is_change_payment_method_request && is_wc_endpoint_url( 'order-pay' ) ) { + return $available_gateways; + } + + $renewal_order_cart_item = wcs_cart_contains_failed_renewal_order_payment(); + $cart_contains_failed_renewal = (bool) $renewal_order_cart_item; + $cart_contains_failed_manual_renewal = false; + + /** + * If there's no change payment request and the cart contains a failed renewal order, check if the subscription is manual. + * + * We update failing, non-manual subscriptions in @see WC_Subscriptions_Change_Payment_Gateway::change_failing_payment_method() so we + * don't need to apply our available payment gateways filter if the subscription is manual. + */ + if ( ! $is_change_payment_method_request && $cart_contains_failed_renewal ) { + $subscription = wcs_get_subscription( $renewal_order_cart_item['subscription_renewal']['subscription_id'] ); + $cart_contains_failed_manual_renewal = $subscription->is_manual(); + } + + if ( apply_filters( 'wcs_payment_gateways_change_payment_method', $is_change_payment_method_request || ( $cart_contains_failed_renewal && ! $cart_contains_failed_manual_renewal ) ) ) { foreach ( $available_gateways as $gateway_id => $gateway ) { if ( true !== $gateway->supports( 'subscription_payment_method_change_customer' ) ) { unset( $available_gateways[ $gateway_id ] ); @@ -555,6 +590,42 @@ class WC_Subscriptions_Change_Payment_Gateway { return $title; } + /** + * Replace the breadcrumbs structure to add a link to the subscription page and change the current page to "Change Payment Method" + * + * @param array $crumbs + * @return array + * @since 2.4.2 + */ + public static function change_payment_method_breadcrumb( $crumbs ) { + + if ( is_main_query() && is_page() && is_checkout_pay_page() && self::$is_request_to_change_payment ) { + global $wp_query; + $subscription = wcs_get_subscription( absint( $wp_query->query_vars['order-pay'] ) ); + + if ( ! $subscription ) { + return $crumbs; + } + + $crumbs[1] = array( + get_the_title( wc_get_page_id( 'myaccount' ) ), + get_permalink( wc_get_page_id( 'myaccount' ) ), + ); + + $crumbs[2] = array( + sprintf( _x( 'Subscription #%s', 'hash before order number', 'woocommerce-subscriptions' ), $subscription->get_order_number() ), + esc_url( $subscription->get_view_order_url() ), + ); + + $crumbs[3] = array( + _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ), + '', + ); + } + + return $crumbs; + } + /** * When processing a change_payment_method request on a subscription that has a failed or pending renewal, * we don't want the `$order->needs_payment()` check inside WC_Shortcode_Checkout::order_pay() to pass. @@ -640,4 +711,3 @@ class WC_Subscriptions_Change_Payment_Gateway { return $subscription_can_be_changed; } } -WC_Subscriptions_Change_Payment_Gateway::init(); diff --git a/includes/class-wc-subscriptions-checkout.php b/includes/class-wc-subscriptions-checkout.php old mode 100755 new mode 100644 index 5c46076..51c5f6f --- a/includes/class-wc-subscriptions-checkout.php +++ b/includes/class-wc-subscriptions-checkout.php @@ -505,5 +505,3 @@ class WC_Subscriptions_Checkout { return $actions; } } - -WC_Subscriptions_Checkout::init(); diff --git a/includes/class-wc-subscriptions-coupon.php b/includes/class-wc-subscriptions-coupon.php old mode 100755 new mode 100644 index 5478aba..c7add64 --- a/includes/class-wc-subscriptions-coupon.php +++ b/includes/class-wc-subscriptions-coupon.php @@ -188,7 +188,7 @@ class WC_Subscriptions_Coupon { $coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' ); // Only deal with subscriptions coupon types which apply to cart items - if ( ! in_array( $coupon_type, array( 'recurring_fee', 'recurring_percent', 'sign_up_fee', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) { + if ( ! in_array( $coupon_type, array( 'recurring_fee', 'recurring_percent', 'sign_up_fee', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart', 'initial_cart' ) ) ) { return $discount; } @@ -227,7 +227,7 @@ class WC_Subscriptions_Coupon { // If all items have a free trial we don't need to apply recurring coupons to the initial total if ( $is_switch || ! WC_Subscriptions_Cart::all_cart_items_have_free_trial() ) { - if ( 'recurring_fee' == $coupon_type ) { + if ( 'recurring_fee' === $coupon_type || 'initial_cart' === $coupon_type ) { $apply_initial_coupon = true; } @@ -437,7 +437,7 @@ class WC_Subscriptions_Coupon { $coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' ); // ignore non-subscription coupons - if ( ! in_array( $coupon_type, array( 'recurring_fee', 'sign_up_fee', 'recurring_percent', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) { + if ( ! in_array( $coupon_type, array( 'recurring_fee', 'sign_up_fee', 'recurring_percent', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart', 'initial_cart' ) ) ) { // but make sure there is actually something for the coupon to be applied to (i.e. not a free trial) if ( ( wcs_cart_contains_renewal() || WC_Subscriptions_Cart::cart_contains_subscription() ) && 0 == WC()->cart->subtotal ) { @@ -496,7 +496,7 @@ class WC_Subscriptions_Coupon { $error_message = sprintf( __( 'Sorry, "%s" can only be applied to subscription parent orders which contain a product with signup fees.', 'woocommerce-subscriptions' ), wcs_get_coupon_property( $coupon, 'code' ) ); // Only recurring coupons can be applied to subscriptions } elseif ( ! in_array( $coupon_type, array( 'recurring_fee', 'recurring_percent' ) ) && wcs_is_subscription( $order ) ) { - $error_message = __( 'Sorry, only recurring coupons can only be applied to subscriptions.', 'woocommerce-subscriptions' ); + $error_message = __( 'Sorry, only recurring coupons can be applied to subscriptions.', 'woocommerce-subscriptions' ); } if ( ! empty( $error_message ) ) { @@ -597,7 +597,7 @@ class WC_Subscriptions_Coupon { public static function filter_product_coupon_types( $product_coupon_types ) { if ( is_array( $product_coupon_types ) ) { - $product_coupon_types = array_merge( $product_coupon_types, array( 'recurring_fee', 'recurring_percent', 'sign_up_fee', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ); + $product_coupon_types = array_merge( $product_coupon_types, array( 'recurring_fee', 'recurring_percent', 'sign_up_fee', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart', 'initial_cart' ) ); } return $product_coupon_types; @@ -681,6 +681,7 @@ class WC_Subscriptions_Coupon { 'renewal_percent' => __( 'Renewal % discount', 'woocommerce-subscriptions' ), 'renewal_fee' => __( 'Renewal product discount', 'woocommerce-subscriptions' ), 'renewal_cart' => __( 'Renewal cart discount', 'woocommerce-subscriptions' ), + 'initial_cart' => __( 'Initial payment discount', 'woocommerce-subscriptions' ), ) ); } @@ -694,10 +695,14 @@ class WC_Subscriptions_Coupon { * @since 2.2.8 */ public static function get_pseudo_coupon_label( $label, $coupon ) { - // If the coupon is one of our pseudo coupons, rather than displaying "Coupon: discount_renewal" display a nicer label. - if ( 'renewal_cart' === wcs_get_coupon_property( $coupon, 'discount_type' ) ) { - $label = esc_html( __( 'Renewal Discount', 'woocommerce-subscriptions' ) ); + switch ( $coupon->get_discount_type() ) { + case 'renewal_cart': + $label = esc_html( __( 'Renewal Discount', 'woocommerce-subscriptions' ) ); + break; + case 'initial_cart': + $label = esc_html( __( 'Discount', 'woocommerce-subscriptions' ) ); + break; } return $label; @@ -1253,5 +1258,3 @@ class WC_Subscriptions_Coupon { _deprecated_function( __METHOD__, '2.0', 'WooCommerce 2.3 removed after tax discounts. Use ' . __CLASS__ .'::apply_subscription_discount( $original_price, $cart_item, $cart )' ); } } - -WC_Subscriptions_Coupon::init(); diff --git a/includes/class-wc-subscriptions-email.php b/includes/class-wc-subscriptions-email.php old mode 100755 new mode 100644 index 722b0d3..dbca723 --- a/includes/class-wc-subscriptions-email.php +++ b/includes/class-wc-subscriptions-email.php @@ -36,17 +36,6 @@ class WC_Subscriptions_Email { * @since 1.4 */ public static function add_emails( $email_classes ) { - - require_once( 'emails/class-wcs-email-new-renewal-order.php' ); - require_once( 'emails/class-wcs-email-new-switch-order.php' ); - require_once( 'emails/class-wcs-email-customer-processing-renewal-order.php' ); - require_once( 'emails/class-wcs-email-customer-completed-renewal-order.php' ); - require_once( 'emails/class-wcs-email-customer-completed-switch-order.php' ); - require_once( 'emails/class-wcs-email-customer-renewal-invoice.php' ); - require_once( 'emails/class-wcs-email-cancelled-subscription.php' ); - require_once( 'emails/class-wcs-email-expired-subscription.php' ); - require_once( 'emails/class-wcs-email-on-hold-subscription.php' ); - $email_classes['WCS_Email_New_Renewal_Order'] = new WCS_Email_New_Renewal_Order(); $email_classes['WCS_Email_New_Switch_Order'] = new WCS_Email_New_Switch_Order(); $email_classes['WCS_Email_Processing_Renewal_Order'] = new WCS_Email_Processing_Renewal_Order(); @@ -363,5 +352,3 @@ class WC_Subscriptions_Email { _deprecated_function( __FUNCTION__, '2.0' ); } } - -WC_Subscriptions_Email::init(); diff --git a/includes/class-wc-subscriptions-manager.php b/includes/class-wc-subscriptions-manager.php old mode 100755 new mode 100644 index fc42da0..86e216d --- a/includes/class-wc-subscriptions-manager.php +++ b/includes/class-wc-subscriptions-manager.php @@ -2360,5 +2360,3 @@ class WC_Subscriptions_Manager { _deprecated_function( __METHOD__, '2.0' ); } } - -WC_Subscriptions_Manager::init(); diff --git a/includes/class-wc-subscriptions-order.php b/includes/class-wc-subscriptions-order.php old mode 100755 new mode 100644 index 6e2456b..b23c47e --- a/includes/class-wc-subscriptions-order.php +++ b/includes/class-wc-subscriptions-order.php @@ -485,12 +485,12 @@ class WC_Subscriptions_Order { // Do we need to activate a subscription? if ( $order_completed && ! $subscription->has_status( wcs_get_subscription_ended_statuses() ) && ! $subscription->has_status( 'active' ) ) { - $new_start_date_offset = current_time( 'timestamp', true ) - $subscription->get_time( 'date_created' ); + $new_start_date_offset = current_time( 'timestamp', true ) - $subscription->get_time( 'start' ); // if the payment has been processed more than an hour after the order was first created, let's update the dates on the subscription to account for that, because it may have even been processed days after it was first placed - if ( $new_start_date_offset > HOUR_IN_SECONDS ) { + if ( abs( $new_start_date_offset ) > HOUR_IN_SECONDS ) { - $dates = array( 'date_created' => current_time( 'mysql', true ) ); + $dates = array( 'start' => current_time( 'mysql', true ) ); if ( WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription ) ) { @@ -498,15 +498,15 @@ class WC_Subscriptions_Order { $next_payment = $subscription->get_time( 'next_payment' ); // if either there is a free trial date or a next payment date that falls before now, we need to recalculate all the sync'd dates - if ( ( $trial_end > 0 && $trial_end < wcs_date_to_time( $dates['date_created'] ) ) || ( $next_payment > 0 && $next_payment < wcs_date_to_time( $dates['date_created'] ) ) ) { + if ( ( $trial_end > 0 && $trial_end < wcs_date_to_time( $dates['start'] ) ) || ( $next_payment > 0 && $next_payment < wcs_date_to_time( $dates['start'] ) ) ) { foreach ( $subscription->get_items() as $item ) { $product_id = wcs_get_canonical_product_id( $item ); if ( WC_Subscriptions_Synchroniser::is_product_synced( $product_id ) ) { - $dates['trial_end'] = WC_Subscriptions_Product::get_trial_expiration_date( $product_id, $dates['date_created'] ); - $dates['next_payment'] = WC_Subscriptions_Synchroniser::calculate_first_payment_date( $product_id, 'mysql', $dates['date_created'] ); - $dates['end'] = WC_Subscriptions_Product::get_expiration_date( $product_id, $dates['date_created'] ); + $dates['trial_end'] = WC_Subscriptions_Product::get_trial_expiration_date( $product_id, $dates['start'] ); + $dates['next_payment'] = WC_Subscriptions_Synchroniser::calculate_first_payment_date( $product_id, 'mysql', $dates['start'] ); + $dates['end'] = WC_Subscriptions_Product::get_expiration_date( $product_id, $dates['start'] ); break; } } @@ -2120,4 +2120,3 @@ class WC_Subscriptions_Order { return wcs_get_objects_property( $order, 'currency' ); } } -WC_Subscriptions_Order::init(); diff --git a/includes/class-wc-subscriptions-product.php b/includes/class-wc-subscriptions-product.php old mode 100755 new mode 100644 index 454d0d4..b12dd56 --- a/includes/class-wc-subscriptions-product.php +++ b/includes/class-wc-subscriptions-product.php @@ -361,7 +361,7 @@ class WC_Subscriptions_Product { } } else { // translators: 1$: recurring amount, 2$: subscription period (e.g. "month" or "3 months") (e.g. "$15 / month" or "$15 every 2nd month") - $subscription_string = sprintf( _n( '%1$s / %2$s', ' %1$s every %2$s', $billing_interval, 'woocommerce-subscriptions' ), $price, wcs_get_subscription_period_strings( $billing_interval, $billing_period ) ); + $subscription_string = sprintf( _n( '%1$s / %2$s', '%1$s every %2$s', $billing_interval, 'woocommerce-subscriptions' ), $price, wcs_get_subscription_period_strings( $billing_interval, $billing_period ) ); } } elseif ( $include['subscription_price'] ) { $subscription_string = $price; @@ -398,7 +398,7 @@ class WC_Subscriptions_Product { * Returns the active price per period for a product if it is a subscription. * * @param mixed $product A WC_Product object or product ID - * @return float The price charged per period for the subscription, or an empty string if the product is not a subscription. + * @return string The price charged per period for the subscription, or an empty string if the product is not a subscription. * @since 1.0 */ public static function get_price( $product ) { @@ -420,7 +420,7 @@ class WC_Subscriptions_Product { * Returns the sale price per period for a product if it is a subscription. * * @param mixed $product A WC_Product object or product ID - * @return float + * @return string * @since 2.2.0 */ public static function get_regular_price( $product, $context = 'view' ) { @@ -438,7 +438,7 @@ class WC_Subscriptions_Product { * Returns the regular price per period for a product if it is a subscription. * * @param mixed $product A WC_Product object or product ID - * @return float + * @return string * @since 2.2.0 */ public static function get_sale_price( $product, $context = 'view' ) { @@ -511,7 +511,7 @@ class WC_Subscriptions_Product { * Returns the sign-up fee for a subscription, if it is a subscription. * * @param mixed $product A WC_Product object or product ID - * @return float The value of the sign-up fee, or 0 if the product is not a subscription or the subscription has no sign-up fee + * @return int|string The value of the sign-up fee, or 0 if the product is not a subscription or the subscription has no sign-up fee * @since 1.0 */ public static function get_sign_up_fee( $product ) { @@ -1248,5 +1248,3 @@ class WC_Subscriptions_Product { return wcs_get_price_excluding_tax( $product, array( 'qty' => $qty, 'price' => WC_Subscriptions_Product::get_sign_up_fee( $product ) ) ); } } - -WC_Subscriptions_Product::init(); diff --git a/includes/class-wc-subscriptions-renewal-order.php b/includes/class-wc-subscriptions-renewal-order.php old mode 100755 new mode 100644 index 4af4008..7af7464 --- a/includes/class-wc-subscriptions-renewal-order.php +++ b/includes/class-wc-subscriptions-renewal-order.php @@ -661,4 +661,3 @@ class WC_Subscriptions_Renewal_Order { _deprecated_function( __METHOD__, '2.0', __CLASS__ . '::maybe_record_subscription_payment( $order_id, $orders_old_status, $orders_new_status )' ); } } -WC_Subscriptions_Renewal_Order::init(); diff --git a/includes/class-wc-subscriptions-switcher.php b/includes/class-wc-subscriptions-switcher.php old mode 100755 new mode 100644 index e68b401..7cd7fff --- a/includes/class-wc-subscriptions-switcher.php +++ b/includes/class-wc-subscriptions-switcher.php @@ -21,7 +21,7 @@ class WC_Subscriptions_Switcher { add_action( 'woocommerce_loaded', __CLASS__ . '::attach_dependant_hooks' ); // Check if the current request is for switching a subscription and if so, start he switching process - add_filter( 'template_redirect', __CLASS__ . '::subscription_switch_handler', 100 ); + add_action( 'template_redirect', __CLASS__ . '::subscription_switch_handler', 100 ); // Pass in the filter switch to the group items add_filter( 'woocommerce_grouped_product_list_link', __CLASS__ . '::add_switch_query_arg_grouped', 12 ); @@ -31,7 +31,7 @@ class WC_Subscriptions_Switcher { add_filter( 'woocommerce_subscription_settings', __CLASS__ . '::add_settings' ); // Add the "Switch" button to the View Subscription table - add_filter( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10, 3 ); + add_action( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10, 3 ); // We need to create subscriptions on checkout and want to do it after almost all other extensions have added their products/items/fees add_action( 'woocommerce_checkout_order_processed', __CLASS__ . '::process_checkout', 50, 2 ); @@ -46,13 +46,13 @@ class WC_Subscriptions_Switcher { add_filter( 'woocommerce_add_to_cart_validation', __CLASS__ . '::validate_switch_request', 10, 4 ); // Record subscription switching in the cart - add_action( 'woocommerce_add_cart_item_data', __CLASS__ . '::set_switch_details_in_cart', 10, 3 ); + add_filter( 'woocommerce_add_cart_item_data', __CLASS__ . '::set_switch_details_in_cart', 10, 3 ); // Make sure the 'switch_subscription' cart item data persists - add_action( 'woocommerce_get_cart_item_from_session', __CLASS__ . '::get_cart_from_session', 10, 3 ); + add_filter( 'woocommerce_get_cart_item_from_session', __CLASS__ . '::get_cart_from_session', 10, 3 ); // Set totals for subscription switch orders (needs to be hooked just before WC_Subscriptions_Cart::calculate_subscription_totals()) - add_filter( 'woocommerce_before_calculate_totals', __CLASS__ . '::calculate_prorated_totals', 99, 1 ); + add_action( 'woocommerce_before_calculate_totals', __CLASS__ . '::calculate_prorated_totals', 99, 1 ); // Don't display free trials when switching a subscription, because no free trials are provided add_filter( 'woocommerce_subscriptions_product_price_string_inclusions', __CLASS__ . '::customise_product_string_inclusions', 12, 2 ); @@ -73,7 +73,7 @@ class WC_Subscriptions_Switcher { add_action( 'addons_add_to_cart_url', __CLASS__ . '::addons_add_to_cart_url', 10 ); // Make sure the switch process persists when having to choose product addons - add_action( 'woocommerce_hidden_order_itemmeta', __CLASS__ . '::hidden_order_itemmeta', 10 ); + add_filter( 'woocommerce_hidden_order_itemmeta', __CLASS__ . '::hidden_order_itemmeta', 10 ); // Add/remove the print switch link filters when printing HTML/plain subscription emails add_action( 'woocommerce_email_before_subscription_table', __CLASS__ . '::remove_print_switch_link' ); @@ -101,7 +101,7 @@ class WC_Subscriptions_Switcher { add_filter( 'woocommerce_cart_needs_payment', __CLASS__ . '::cart_needs_payment' , 50, 2 ); // Require payment when switching from a $0 / period subscription to a non-zero subscription to process automatic payments - add_filter( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::maybe_set_payment_method_after_switch' , 10, 1 ); + add_action( 'woocommerce_subscriptions_switch_completed', __CLASS__ . '::maybe_set_payment_method_after_switch' , 10, 1 ); // Do not reduce product stock when the order item is simply to record a switch add_filter( 'woocommerce_order_item_quantity', __CLASS__ . '::maybe_do_not_reduce_stock', 10, 3 ); @@ -1152,7 +1152,7 @@ class WC_Subscriptions_Switcher { if ( ! current_user_can( 'switch_shop_subscription', $subscription->get_id() ) ) { wc_add_notice( __( 'You can not switch this subscription. It appears you do not own the subscription.', 'woocommerce-subscriptions' ), 'error' ); WC()->cart->empty_cart( true ); - wp_redirect( get_permalink( $subscription['product_id'] ) ); + wp_redirect( get_permalink( $product_id ) ); exit(); } @@ -1293,15 +1293,15 @@ class WC_Subscriptions_Switcher { continue; } - $subscription = wcs_get_subscription( $cart_item['subscription_switch']['subscription_id'] ); - $existing_item = wcs_get_order_item( $cart_item['subscription_switch']['item_id'], $subscription ); + $subscription = wcs_get_subscription( $cart_item['subscription_switch']['subscription_id'] ); + $existing_item = wcs_get_order_item( $cart_item['subscription_switch']['item_id'], $subscription ); if ( empty( $existing_item ) ) { WC()->cart->remove_cart_item( $cart_item_key ); continue; } - $item_data = $cart_item['data']; + $product_in_cart = $cart_item['data']; $product_id = wcs_get_canonical_product_id( $cart_item ); $product = wc_get_product( $product_id ); $is_virtual_product = $product->is_virtual(); @@ -1345,13 +1345,15 @@ class WC_Subscriptions_Switcher { $days_until_next_payment = ceil( ( $next_payment_timestamp - gmdate( 'U' ) ) / ( 60 * 60 * 24 ) ); // If the subscription contains a synced product and the next payment is actually the first payment, determine the days in the "old" cycle from the subscription object - if ( WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription->get_id() ) && WC_Subscriptions_Synchroniser::calculate_first_payment_date( $product, 'timestamp', $subscription->get_date( 'date_created' ) ) == $next_payment_timestamp ) { + if ( WC_Subscriptions_Synchroniser::subscription_contains_synced_product( $subscription->get_id() ) && WC_Subscriptions_Synchroniser::calculate_first_payment_date( $product, 'timestamp', $subscription->get_date( 'start' ) ) == $next_payment_timestamp ) { $days_in_old_cycle = wcs_get_days_in_cycle( $subscription->get_billing_period(), $subscription->get_billing_interval() ); } else { // Find the number of days between the two $days_in_old_cycle = $days_until_next_payment + $days_since_last_payment; } + $days_in_old_cycle = apply_filters( 'wcs_switch_proration_days_in_old_cycle', $days_in_old_cycle, $subscription, $cart_item ); + // Find the actual recurring amount charged for the old subscription (we need to use the '_recurring_line_total' meta here rather than '_subscription_recurring_amount' because we want the recurring amount to include extra from extensions, like Product Add-ons etc.) $old_recurring_total = $existing_item['line_total']; @@ -1371,15 +1373,16 @@ class WC_Subscriptions_Switcher { // Find the $price per day for the old subscription's recurring total $old_price_per_day = $days_in_old_cycle > 0 ? $old_recurring_total / $days_in_old_cycle : $old_recurring_total; + $old_price_per_day = apply_filters( 'wcs_switch_proration_old_price_per_day', $old_price_per_day, $subscription, $cart_item, $old_recurring_total, $days_in_old_cycle ); // Find the price per day for the new subscription's recurring total based on billing schedule - $days_in_new_cycle = wcs_get_days_in_cycle( WC_Subscriptions_Product::get_period( $item_data ), WC_Subscriptions_Product::get_interval( $item_data ) ); + $days_in_new_cycle = wcs_get_days_in_cycle( WC_Subscriptions_Product::get_period( $product_in_cart ), WC_Subscriptions_Product::get_interval( $product_in_cart ) ); // Whether the days in new cycle match the days in old,ignoring any rounding. $days_in_new_and_old_cycle_match = ceil( $days_in_new_cycle ) == $days_in_old_cycle || floor( $days_in_new_cycle ) == $days_in_old_cycle; // Whether the new item uses the same billing interval & cycle as the old subscription, - $matching_billing_cycle = WC_Subscriptions_Product::get_period( $item_data ) == $subscription->get_billing_period() && WC_Subscriptions_Product::get_interval( $item_data ) == $subscription->get_billing_interval(); + $matching_billing_cycle = WC_Subscriptions_Product::get_period( $product_in_cart ) == $subscription->get_billing_period() && WC_Subscriptions_Product::get_interval( $product_in_cart ) == $subscription->get_billing_interval(); $switch_during_trial = $subscription->get_time( 'trial_end' ) > gmdate( 'U' ); // Set the days in each cycle to match if they are equal (ignoring any rounding discrepancy) or if the subscription is switched during a trial and has a matching billing cycle. @@ -1387,29 +1390,41 @@ class WC_Subscriptions_Switcher { $days_in_new_cycle = $days_in_old_cycle; } + $days_in_new_cycle = apply_filters( 'wcs_switch_proration_days_in_new_cycle', $days_in_new_cycle, $subscription, $cart_item, $days_in_old_cycle ); + // We need to use the cart items price to ensure we include extras added by extensions like Product Add-ons, but we don't want the sign-up fee accounted for in the price, so make sure WC_Subscriptions_Cart::set_subscription_prices_for_calculation() isn't adding that. remove_filter( 'woocommerce_product_get_price', 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation', 100 ); - $new_price_per_day = ( WC_Subscriptions_Product::get_price( $item_data ) * $cart_item['quantity'] ) / $days_in_new_cycle; + $new_price_per_day = ( WC_Subscriptions_Product::get_price( $product_in_cart ) * $cart_item['quantity'] ) / $days_in_new_cycle; add_filter( 'woocommerce_product_get_price', 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation', 100, 2 ); + $new_price_per_day = apply_filters( 'wcs_switch_proration_new_price_per_day', $new_price_per_day, $subscription, $cart_item, $days_in_new_cycle ); + if ( $old_price_per_day < $new_price_per_day ) { - - WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['upgraded_or_downgraded'] = 'upgraded'; - + $switch_type = 'upgrade'; } elseif ( $old_price_per_day > $new_price_per_day && $new_price_per_day >= 0 ) { - - WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['upgraded_or_downgraded'] = 'downgraded'; - + $switch_type = 'downgrade'; + } else { + $switch_type = 'crossgrade'; } + $switch_type = apply_filters( 'wcs_switch_proration_switch_type', $switch_type, $subscription, $cart_item, $old_price_per_day, $new_price_per_day ); + + if ( ! in_array( $switch_type, array( 'upgrade', 'downgrade', 'crossgrade' ) ) ) { + throw new UnexpectedValueException( sprintf( __( 'Invalid switch type "%s". Switch must be one of: "upgrade", "downgrade" or "crossgrade".', 'woocommerce-subscriptions' ), $switch_type ) ); + } + + WC()->cart->cart_contents[ $cart_item_key ]['subscription_switch']['upgraded_or_downgraded'] = sprintf( '%sd', $switch_type ); // preserve past tense for backward compatibility (luckily past tense for all allowed switch types end with a d) + // Now lets see if we should add a prorated amount to the sign-up fee (for upgrades) or extend the next payment date (for downgrades) if ( in_array( $apportion_recurring_price, array( 'yes', 'yes-upgrade' ) ) || ( in_array( $apportion_recurring_price, array( 'virtual', 'virtual-upgrade' ) ) && $is_virtual_product ) ) { // If the customer is upgrading, we may need to add a gap payment to the sign-up fee or to reduce the pre-paid period (or both) - if ( $old_price_per_day < $new_price_per_day ) { + if ( 'upgrade' === $switch_type ) { - // The new subscription may be more expensive, but it's also on a shorter billing cycle, so reduce the next pre-paid term - if ( $days_in_old_cycle > $days_in_new_cycle ) { + // The new subscription may be more expensive, but it's also on a shorter billing cycle, so reduce the next pre-paid term by default, but also allow this to be customised + $reduce_pre_paid_term = apply_filters( 'wcs_switch_proration_reduce_pre_paid_term', $days_in_old_cycle > $days_in_new_cycle, $subscription, $cart_item, $days_in_old_cycle, $days_in_new_cycle, $old_price_per_day, $new_price_per_day ); + + if ( $reduce_pre_paid_term ) { // Find out how many days at the new price per day the customer would receive for the total amount already paid // (e.g. if the customer paid $10 / month previously, and was switching to a $5 / week subscription, she has pre-paid 14 days at the new price) @@ -1437,7 +1452,7 @@ class WC_Subscriptions_Switcher { $extra_to_pay = $days_until_next_payment * ( $new_price_per_day - $old_price_per_day ); // when calculating a subscription with one length (no more next payment date and the end date may have been pushed back) we need to pay for those extra days at the new price per day between the old next payment date and new end date - if ( 1 == WC_Subscriptions_Product::get_length( $item_data ) ) { + if ( 1 == WC_Subscriptions_Product::get_length( $product_in_cart ) ) { $days_to_new_end = floor( ( $end_timestamp - $next_payment_timestamp ) / ( 60 * 60 * 24 ) ); if ( $days_to_new_end > 0 ) { @@ -1447,6 +1462,7 @@ class WC_Subscriptions_Switcher { // We need to find the per item extra to pay so we can set it as the sign-up fee (WC will then multiply it by the quantity) $extra_to_pay = $extra_to_pay / $cart_item['quantity']; + $extra_to_pay = apply_filters( 'wcs_switch_proration_extra_to_pay', $extra_to_pay, $subscription, $cart_item, $days_in_old_cycle ); // Keep a record of the two separate amounts so we store these and calculate future switch amounts correctly $existing_sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( WC()->cart->cart_contents[ $cart_item_key ]['data'] ); @@ -1456,7 +1472,7 @@ class WC_Subscriptions_Switcher { } // If the customer is downgrading, set the next payment date and maybe extend it if downgrades are prorated - } elseif ( $old_price_per_day > $new_price_per_day && $new_price_per_day > 0 ) { + } elseif ( 'downgrade' === $switch_type ) { $old_total_paid = $old_price_per_day * $days_until_next_payment; @@ -1875,43 +1891,40 @@ class WC_Subscriptions_Switcher { } if ( ! empty( $switch_data['switches'] ) && is_array( $switch_data['switches'] ) ) { + foreach ( $switch_data['switches'] as $order_item_id => $switched_item_data ) { - // If the switch data is in the old format - if ( ! array_key_exists( 'remove_line_item', reset( $switch_data['switches'] ) ) ) { - self::switch_line_items_pre_2_1_2( $switch_data['switches'], $order, $subscription ); - } else { - foreach ( $switch_data['switches'] as $order_item_id => $switched_item_data ) { + // If we are adding a line item to an existing subscription + if ( isset( $switched_item_data['add_line_item'] ) ) { + wcs_update_order_item_type( $switched_item_data['add_line_item'], 'line_item', $subscription->get_id() ); + do_action( 'woocommerce_subscription_item_switched', $order, $subscription, $switched_item_data['add_line_item'], $switched_item_data['remove_line_item'] ); + } - // If we are adding a line item to an existing subscription - if ( isset( $switched_item_data['add_line_item'] ) ) { - wcs_update_order_item_type( $switched_item_data['add_line_item'], 'line_item', $subscription->get_id() ); - do_action( 'woocommerce_subscription_item_switched', $order, $subscription, $switched_item_data['add_line_item'], $switched_item_data['remove_line_item'] ); - } + // remove the existing subscription item + $old_subscription_item = wcs_get_order_item( $switched_item_data['remove_line_item'], $subscription ); + $switch_order_item = wcs_get_order_item( $order_item_id, $order ); - // remove the existing subscription item - $old_subscription_item = wcs_get_order_item( $switched_item_data['remove_line_item'], $subscription ); - $switch_order_item = wcs_get_order_item( $order_item_id, $order ); + if ( empty( $old_subscription_item ) ) { + throw new Exception( __( 'The original subscription item being switched cannot be found.', 'woocommerce-subscriptions' ) ); + } elseif ( empty( $switch_order_item ) ) { + throw new Exception( __( 'The item on the switch order cannot be found.', 'woocommerce-subscriptions' ) ); + } else { + // We don't want to include switch item meta in order item name + add_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); + $old_item_name = wcs_get_order_item_name( $old_subscription_item, array( 'attributes' => true ) ); + $new_item_name = wcs_get_order_item_name( $switch_order_item, array( 'attributes' => true ) ); + remove_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); - if ( empty( $old_subscription_item ) ) { - throw new Exception( __( 'The original subscription item being switched cannot be found.', 'woocommerce-subscriptions' ) ); - } elseif ( empty( $switch_order_item ) ) { - throw new Exception( __( 'The item on the switch order cannot be found.', 'woocommerce-subscriptions' ) ); - } else { - // We don't want to include switch item meta in order item name - add_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); - $old_item_name = wcs_get_order_item_name( $old_subscription_item, array( 'attributes' => true ) ); - $new_item_name = wcs_get_order_item_name( $switch_order_item, array( 'attributes' => true ) ); - remove_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); + wcs_update_order_item_type( $switched_item_data['remove_line_item'], 'line_item_switched', $subscription->get_id() ); - wcs_update_order_item_type( $switched_item_data['remove_line_item'], 'line_item_switched', $subscription->get_id() ); - - // translators: 1$: old item, 2$: new item when switching - $add_note = sprintf( _x( 'Customer switched from: %1$s to %2$s.', 'used in order notes', 'woocommerce-subscriptions' ), $old_item_name, $new_item_name ); - } + // translators: 1$: old item, 2$: new item when switching + $add_note = sprintf( _x( 'Customer switched from: %1$s to %2$s.', 'used in order notes', 'woocommerce-subscriptions' ), $old_item_name, $new_item_name ); } } } + // Subscription objects hold an internal cache of line items so we need to get an updated subscription object after changing the line item types directly in the database. + $subscription = wcs_get_subscription( $subscription_id ); + if ( ! empty( $add_note ) ) { $subscription->add_order_note( $add_note ); } @@ -1942,11 +1955,7 @@ class WC_Subscriptions_Switcher { } } - // If the shipping data is in the old format - if ( ! empty( $switch_data['shipping_methods'] ) ) { - self::switch_shipping_line_items_pre_2_1_2( $subscription, $switch_data['shipping_methods'] ); - } else if ( ! empty( $switch_data['shipping_line_items'] ) && is_array( $switch_data['shipping_line_items'] ) ) { - + if ( ! empty( $switch_data['shipping_line_items'] ) && is_array( $switch_data['shipping_line_items'] ) ) { // Archive the old subscription shipping methods foreach ( $subscription->get_shipping_methods() as $shipping_line_item_id => $item ) { wcs_update_order_item_type( $shipping_line_item_id, 'shipping_switched', $subscription->get_id() ); @@ -2199,98 +2208,6 @@ class WC_Subscriptions_Switcher { return $total; } - /** - * Switch subscription line items provided line item data in the 2.1 switch order meta format. - * - * @param array $switches an array of switch items and its meta - * @param WC_Order $order the switch order - * @param WC_Subscription $subscription the subscription being switched - * @since 2.1.2 - */ - protected static function switch_line_items_pre_2_1_2( $switches, $order, $subscription ) { - - foreach ( $switches as $order_item_id => $switch_item_data ) { - - $order_item = wcs_get_order_item( $order_item_id, $order ); - - // if we are simply adding this product to an existing subscription - if ( isset( $switch_item_data['add_order_item_data'] ) ) { - $product = WC_Subscriptions::get_product( wcs_get_canonical_product_id( $order_item ) ); - $line_tax_data = wc_get_order_item_meta( $order_item_id, '_line_tax_data', true ); - $variation_attributes = ( method_exists( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array(); - - $item_id = $subscription->add_product( $product, $order_item['qty'], array( - 'variation' => $variation_attributes, - 'totals' => $switch_item_data['add_order_item_data']['totals'], - ) ); - - foreach ( $switch_item_data['add_order_item_data']['meta'] as $key => $value ) { - if ( ! array_key_exists( 'attribute_' . $key, $variation_attributes ) ) { - wc_add_order_item_meta( $item_id, $key, reset( $value ), true ); - } - } - - do_action( 'woocommerce_subscription_item_switched', $order, $subscription, $order_item_id, $switch_item_data['subscription_item_id'] ); - } - - // remove the existing subscription item - $old_order_item = wcs_get_order_item( $switch_item_data['subscription_item_id'], $subscription ); - - if ( empty( $old_order_item ) ) { - throw new Exception( __( 'The original subscription item being switched cannot be found.', 'woocommerce-subscriptions' ) ); - } else { - // We don't want to include switch item meta in order item name - add_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); - $new_order_item_name = wcs_get_order_item_name( $order_item, array( 'attributes' => true ) ); - $old_subscription_item_name = wcs_get_order_item_name( $old_order_item, array( 'attributes' => true ) ); - remove_filter( 'woocommerce_subscriptions_hide_switch_itemmeta', '__return_true' ); - - wcs_update_order_item_type( $switch_item_data['subscription_item_id'], 'line_item_switched', $subscription->get_id() ); - - // translators: 1$: old item, 2$: new item when switching - $subscription->add_order_note( sprintf( _x( 'Customer switched from: %1$s to %2$s.', 'used in order notes', 'woocommerce-subscriptions' ), $old_subscription_item_name, $new_order_item_name ) ); - } - } - } - - /** - * Switch subscription shipping line items provided shipping line item data in the 2.1 switch order meta format. - * - * @param WC_Subscription $subscription the subscription being switched - * @param array $shipping_methods an array of shipping line items and meta - * @since 2.1.2 - */ - protected static function switch_shipping_line_items_pre_2_1_2( $subscription, $shipping_methods ) { - // Archive the old subscription shipping methods - foreach ( $subscription->get_shipping_methods() as $shipping_line_item_id => $item ) { - wcs_update_order_item_type( $shipping_line_item_id, 'shipping_switched', $subscription->get_id() ); - } - - // Add the new shipping line item - foreach ( $shipping_methods as $shipping_line_item ) { - $item_id = wc_add_order_item( $subscription->get_id(), array( - 'order_item_name' => $shipping_line_item['name'], - 'order_item_type' => 'shipping', - ) ); - - if ( ! $item_id || empty( $shipping_line_item['method_id'] ) || empty( $shipping_line_item['cost'] ) || empty( $shipping_line_item['taxes'] ) ) { - throw new Exception( __( 'Failed to update the subscription shipping method.', 'woocommerce-subscriptions' ) ); - } - - // Add shipping order item meta - wc_add_order_item_meta( $item_id, 'method_id', $shipping_line_item['method_id'] ); - wc_add_order_item_meta( $item_id, 'cost', wc_format_decimal( $shipping_line_item['cost'] ) ); - - $taxes = array_map( 'wc_format_decimal', maybe_unserialize( $shipping_line_item['taxes'] ) ); - wc_add_order_item_meta( $item_id, 'taxes', $taxes ); - - // Add custom shipping order item meta added by third-party plugins - foreach ( $shipping_line_item['item_meta'] as $key => $value ) { - wc_add_order_item_meta( $item_id, $key, $value ); - } - } - } - /** * Check if a cart item has a different billing schedule (period and interval) to the subscription being switched. * @@ -2699,4 +2616,3 @@ class WC_Subscriptions_Switcher { return $related_orders; } } -WC_Subscriptions_Switcher::init(); diff --git a/includes/class-wc-subscriptions-synchroniser.php b/includes/class-wc-subscriptions-synchroniser.php old mode 100755 new mode 100644 index 44bb735..6d6b668 --- a/includes/class-wc-subscriptions-synchroniser.php +++ b/includes/class-wc-subscriptions-synchroniser.php @@ -590,7 +590,7 @@ class WC_Subscriptions_Synchroniser { public static function calculate_first_payment_date( $product, $type = 'mysql', $from_date = '' ) { if ( ! is_object( $product ) ) { - $product = WC_Subscriptions::get_product( $product ); + $product = wc_get_product( $product ); } if ( ! self::is_product_synced( $product ) ) { @@ -690,7 +690,7 @@ class WC_Subscriptions_Synchroniser { if ( 'year' == $period || 'month' == $period ) { // First make sure the day is in the past so that we don't end up jumping a month or year because of a few hours difference between now and the billing date - if ( gmdate( 'Ymd', $first_payment_timestamp ) < gmdate( 'Ymd', $from_timestamp ) || gmdate( 'Ymd', $first_payment_timestamp ) < gmdate( 'Ymd' ) ) { + if ( gmdate( 'Ymd', $first_payment_timestamp ) < gmdate( 'Ymd', $from_timestamp ) || gmdate( 'Ymd', $first_payment_timestamp ) < gmdate( 'Ymd', current_time( 'timestamp' ) ) ) { $i = 1; // Then make sure the date and time of the payment is in the future while ( ( $first_payment_timestamp < gmdate( 'U' ) || $first_payment_timestamp < $from_timestamp ) && $i < 30 ) { @@ -1515,5 +1515,3 @@ class WC_Subscriptions_Synchroniser { } } -add_action( 'init', 'WC_Subscriptions_Synchroniser::init' ); - diff --git a/includes/class-wcs-action-scheduler.php b/includes/class-wcs-action-scheduler.php old mode 100755 new mode 100644 index bef25b9..9ebd1da --- a/includes/class-wcs-action-scheduler.php +++ b/includes/class-wcs-action-scheduler.php @@ -35,7 +35,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler { $action_args = $this->get_action_args( $date_type, $subscription ); $timestamp = wcs_date_to_time( $datetime ); - $next_scheduled = wc_next_scheduled_action( $action_hook, $action_args ); + $next_scheduled = as_next_scheduled_action( $action_hook, $action_args ); if ( $next_scheduled !== $timestamp ) { @@ -44,7 +44,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler { // Only reschedule if it's in the future if ( $timestamp > current_time( 'timestamp', true ) && ( 'payment_retry' == $date_type || 'active' == $subscription->get_status() ) ) { - wc_schedule_single_action( $timestamp, $action_hook, $action_args ); + as_schedule_single_action( $timestamp, $action_hook, $action_args ); } } } @@ -83,7 +83,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler { } $action_args = $this->get_action_args( $date_type, $subscription ); - $next_scheduled = wc_next_scheduled_action( $action_hook, $action_args ); + $next_scheduled = as_next_scheduled_action( $action_hook, $action_args ); // Maybe clear the existing schedule for this hook if ( false !== $next_scheduled && $next_scheduled != $event_time ) { @@ -91,7 +91,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler { } if ( 0 != $event_time && $event_time > current_time( 'timestamp', true ) && $next_scheduled != $event_time ) { - wc_schedule_single_action( $event_time, $action_hook, $action_args ); + as_schedule_single_action( $event_time, $action_hook, $action_args ); } } break; @@ -104,7 +104,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler { $end_time = $subscription->get_time( 'end' ); // This will have been set to the correct date already $action_args = $this->get_action_args( 'end', $subscription ); - $next_scheduled = wc_next_scheduled_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args ); + $next_scheduled = as_next_scheduled_action( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args ); if ( false !== $next_scheduled && $next_scheduled != $end_time ) { $this->unschedule_actions( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args ); @@ -112,7 +112,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler { // The end date was set in WC_Subscriptions::update_dates() to the appropriate value, so we can schedule our action for that time if ( $end_time > current_time( 'timestamp', true ) && $next_scheduled != $end_time ) { - wc_schedule_single_action( $end_time, 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args ); + as_schedule_single_action( $end_time, 'woocommerce_scheduled_subscription_end_of_prepaid_term', $action_args ); } break; case 'on-hold' : @@ -189,9 +189,6 @@ class WCS_Action_Scheduler extends WCS_Scheduler { * @param array $action_args Array of name => value pairs stored against the scheduled action. */ protected function unschedule_actions( $action_hook, $action_args ) { - do { - wc_unschedule_action( $action_hook, $action_args ); - $next_scheduled = wc_next_scheduled_action( $action_hook, $action_args ); - } while ( false !== $next_scheduled ); + as_unschedule_all_actions( $action_hook, $action_args ); } } diff --git a/includes/class-wcs-api.php b/includes/class-wcs-api.php old mode 100755 new mode 100644 index 1e1ce13..5aab599 --- a/includes/class-wcs-api.php +++ b/includes/class-wcs-api.php @@ -31,10 +31,6 @@ class WCS_API { public static function includes( $wc_api_classes ) { if ( ! defined( 'WC_API_REQUEST_VERSION' ) || 3 == WC_API_REQUEST_VERSION ) { - - require_once( 'api/legacy/class-wc-api-subscriptions.php' ); - require_once( 'api/legacy/class-wc-api-subscriptions-customers.php' ); - array_push( $wc_api_classes, 'WC_API_Subscriptions' ); array_push( $wc_api_classes, 'WC_API_Subscriptions_Customers' ); } @@ -54,14 +50,6 @@ class WCS_API { return; } - if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { - require_once( 'api/legacy/class-wc-rest-subscriptions-controller.php' ); - require_once( 'api/legacy/class-wc-rest-subscription-notes-controller.php' ); - } else { - require_once( 'api/class-wc-rest-subscriptions-controller.php' ); - require_once( 'api/class-wc-rest-subscription-notes-controller.php' ); - } - foreach ( array( 'WC_REST_Subscriptions_Controller', 'WC_REST_Subscription_Notes_Controller' ) as $api_class ) { $controller = new $api_class(); $controller->register_routes(); @@ -69,5 +57,3 @@ class WCS_API { } } - -WCS_API::init(); diff --git a/includes/class-wcs-auth.php b/includes/class-wcs-auth.php old mode 100755 new mode 100644 index 6ff2740..add1336 --- a/includes/class-wcs-auth.php +++ b/includes/class-wcs-auth.php @@ -49,5 +49,3 @@ class WCS_Auth { return $permissions; } } - -return new WCS_Auth(); diff --git a/includes/class-wcs-autoloader.php b/includes/class-wcs-autoloader.php new file mode 100644 index 0000000..3205373 --- /dev/null +++ b/includes/class-wcs-autoloader.php @@ -0,0 +1,285 @@ +base_path = untrailingslashit( $base_path ); + } + + /** + * Destructor. + */ + public function __destruct() { + $this->unregister(); + } + + /** + * Register the autoloader. + * + * @author Jeremy Pry + */ + public function register() { + spl_autoload_register( array( $this, 'autoload' ) ); + } + + /** + * Unregister the autoloader. + */ + public function unregister() { + spl_autoload_unregister( array( $this, 'autoload' ) ); + } + + /** + * Autoload a class. + * + * @author Jeremy Pry + * + * @param string $class The class name to autoload. + */ + public function autoload( $class ) { + $class = strtolower( $class ); + + if ( ! $this->should_autoload( $class ) ) { + return; + } + + $full_path = $this->base_path . $this->get_relative_class_path( $class ) . $this->get_file_name( $class ); + if ( is_readable( $full_path ) ) { + require_once( $full_path ); + } + } + + /** + * Determine whether we should autoload a given class. + * + * @author Jeremy Pry + * + * @param string $class The class name. + * + * @return bool + */ + protected function should_autoload( $class ) { + // We're not using namespaces, so if the class has namespace separators, skip. + if ( false !== strpos( $class, '\\' ) ) { + return false; + } + + // There are some legacy classes without WCS or Subscription in the name. + static $legacy = array( + 'wc_order_item_pending_switch' => 1, + 'wc_report_retention_rate' => 1, + 'wc_report_upcoming_recurring_revenue' => 1, + ); + if ( isset( $legacy[ $class ] ) ) { + return true; + } + + return false !== strpos( $class, 'wcs_' ) || + 0 === strpos( $class, 'wc_subscription' ) || + ( false !== strpos( $class, 'wc_' ) && false !== strpos( $class, 'subscription' ) ); + } + + /** + * Convert the class name into an appropriate file name. + * + * @author Jeremy Pry + * + * @param string $class The class name. + * + * @return string The file name. + */ + protected function get_file_name( $class ) { + $file_prefix = 'class-'; + + if ( $this->is_class_abstract( $class ) ) { + $file_prefix = 'abstract-'; + } elseif ( $this->is_class_interface( $class ) ) { + $file_prefix = 'interface-'; + } + + return $file_prefix . str_replace( '_', '-', $class ) . '.php'; + } + + /** + * Determine if the class is one of our abstract classes. + * + * @author Jeremy Pry + * + * @param string $class The class name. + * + * @return bool + */ + protected function is_class_abstract( $class ) { + static $abstracts = array( + 'wcs_background_updater' => true, + 'wcs_background_upgrader' => true, + 'wcs_cache_manager' => true, + 'wcs_debug_tool' => true, + 'wcs_debug_tool_cache_updater' => true, + 'wcs_dynamic_hook_deprecator' => true, + 'wcs_hook_deprecator' => true, + 'wcs_retry_store' => true, + 'wcs_scheduler' => true, + 'wcs_sv_api_base' => true, + 'wcs_customer_store' => true, + 'wcs_related_order_store' => true, + 'wcs_migrator' => true, + 'wcs_table_maker' => true, + ); + + return isset( $abstracts[ $class ] ); + } + + /** + * Determine if the class is one of our class interfaces. + * + * @param string $class The class name. + + * @return bool + */ + protected function is_class_interface( $class ) { + static $interfaces = array( + 'wcs_cache_updater' => true, + ); + + return isset( $interfaces[ $class ] ); + } + + /** + * Determine if the class is one of our data stores. + * + * @param string $class The class name. + + * @return bool + */ + protected function is_class_data_store( $class ) { + static $data_stores = array( + 'wcs_related_order_store_cached_cpt' => true, + 'wcs_related_order_store_cpt' => true, + 'wcs_customer_store_cached_cpt' => true, + 'wcs_customer_store_cpt' => true, + 'wcs_product_variable_data_store_cpt' => true, + 'wcs_subscription_data_store_cpt' => true, + ); + + return isset( $data_stores[ $class ] ); + } + + /** + * Get the relative path for the class location. + * + * This handles all of the special class locations and exceptions. + * + * @author Jeremy Pry + * + * @param string $class The class name. + * + * @return string The relative path (from the plugin root) to the class file. + */ + protected function get_relative_class_path( $class ) { + $path = '/includes'; + $is_admin = ( false !== strpos( $class, 'admin' ) ); + + if ( $this->is_class_abstract( $class ) ) { + if ( 'wcs_sv_api_base' === $class ) { + $path .= '/gateways/paypal/includes/abstracts'; + } else { + $path .= '/abstracts'; + } + } elseif ( $this->is_class_interface( $class ) ) { + $path .= '/interfaces'; + } elseif ( false !== strpos( $class, 'paypal' ) ) { + $path .= '/gateways/paypal'; + if ( 'wcs_paypal' === $class ) { + $path .= ''; + } elseif ( 'wcs_repair_suspended_paypal_subscriptions' === $class ) { + // Deliberately avoid concatenation for this class, using the base path. + $path = '/includes/upgrades'; + } elseif ( $is_admin ) { + $path .= '/includes/admin'; + } elseif ( 'wc_paypal_standard_subscriptions' === $class ) { + $path .= '/includes/deprecated'; + } else { + $path .= '/includes'; + } + } elseif ( 0 === strpos( $class, 'wcs_retry' ) && 'wcs_retry_manager' !== $class ) { + $path .= '/payment-retry'; + } elseif ( $is_admin && 'wcs_change_payment_method_admin' !== $class ) { + $path .= '/admin'; + } elseif ( false !== strpos( $class, 'meta_box' ) ) { + $path .= '/admin/meta-boxes'; + } elseif ( false !== strpos( $class, 'wc_report' ) ) { + $path .= '/admin/reports/deprecated'; + } elseif ( false !== strpos( $class, 'report' ) ) { + $path .= '/admin/reports'; + } elseif ( false !== strpos( $class, 'debug_tool' ) ) { + $path .= '/admin/debug-tools'; + } elseif ( false !== strpos( $class, 'rest' ) ) { + $path .= $this->legacy_api ? '/api/legacy' : '/api'; + } elseif ( false !== strpos( $class, 'api' ) && 'wcs_api' !== $class ) { + $path .= '/api/legacy'; + } elseif ( $this->is_class_data_store( $class ) ) { + $path .= '/data-stores'; + } elseif ( false !== strpos( $class, 'deprecat' ) ) { + $path .= '/deprecated'; + } elseif ( false !== strpos( $class, 'email' ) && 'wc_subscriptions_email' !== $class ) { + $path .= '/emails'; + } elseif ( false !== strpos( $class, 'gateway' ) && 'wc_subscriptions_change_payment_gateway' !== $class ) { + $path .= '/gateways'; + } elseif ( false !== strpos( $class, 'legacy' ) || 'wcs_array_property_post_meta_black_magic' === $class ) { + $path .= '/legacy'; + } elseif ( false !== strpos( $class, 'privacy' ) ) { + $path .= '/privacy'; + } elseif ( false !== strpos( $class, 'upgrade' ) || false !== strpos( $class, 'repair' ) ) { + $path .= '/upgrades'; + } elseif ( false !== strpos( $class, 'early' ) ) { + $path .= '/early-renewal'; + } + + return trailingslashit( $path ); + } + + /** + * Set whether the legacy API should be used. + * + * @author Jeremy Pry + * + * @param bool $use_legacy_api Whether to use the legacy API classes. + * + * @return $this + */ + public function use_legacy_api( $use_legacy_api ) { + $this->legacy_api = (bool) $use_legacy_api; + + return $this; + } +} diff --git a/includes/class-wcs-cached-data-manager.php b/includes/class-wcs-cached-data-manager.php old mode 100755 new mode 100644 index 98acf45..cdc8c20 --- a/includes/class-wcs-cached-data-manager.php +++ b/includes/class-wcs-cached-data-manager.php @@ -11,6 +11,9 @@ */ class WCS_Cached_Data_Manager extends WCS_Cache_Manager { + /** + * @var WC_Logger_Interface|null + */ public $logger = null; public function __construct() { diff --git a/includes/class-wcs-cart-initial-payment.php b/includes/class-wcs-cart-initial-payment.php old mode 100755 new mode 100644 index 6376f27..06f6bab --- a/includes/class-wcs-cart-initial-payment.php +++ b/includes/class-wcs-cart-initial-payment.php @@ -20,11 +20,12 @@ class WCS_Cart_Initial_Payment extends WCS_Cart_Renewal { * @since 2.0 */ public function __construct() { - $this->setup_hooks(); // When an order is paid for via checkout, ensure a new order isn't created due to mismatched cart hashes add_filter( 'woocommerce_create_order', array( &$this, 'update_cart_hash' ), 10, 1 ); + // Apply initial discounts when there is a pending initial order + add_action( 'woocommerce_setup_cart_for_subscription_initial_payment', array( $this, 'setup_discounts' ) ); } /** @@ -35,48 +36,51 @@ class WCS_Cart_Initial_Payment extends WCS_Cart_Renewal { public function maybe_setup_cart() { global $wp; - if ( isset( $_GET['pay_for_order'] ) && isset( $_GET['key'] ) && isset( $wp->query_vars['order-pay'] ) ) { + if ( ! isset( $_GET['pay_for_order'] ) || ! isset( $_GET['key'] ) || ! isset( $wp->query_vars['order-pay'] ) ) { + return; + } - // Pay for existing order - $order_key = $_GET['key']; - $order_id = ( isset( $wp->query_vars['order-pay'] ) ) ? $wp->query_vars['order-pay'] : absint( $_GET['order_id'] ); - $order = wc_get_order( $wp->query_vars['order-pay'] ); + // Pay for existing order + $order_key = $_GET['key']; + $order_id = absint( $wp->query_vars['order-pay'] ); + $order = wc_get_order( $order_id ); - if ( wcs_get_objects_property( $order, 'order_key' ) == $order_key && $order->has_status( array( 'pending', 'failed' ) ) && wcs_order_contains_subscription( $order, 'parent' ) && ! wcs_order_contains_subscription( $order, 'resubscribe' ) ) { + if ( wcs_get_objects_property( $order, 'order_key' ) !== $order_key || ! $order->has_status( array( 'pending', 'failed' ) ) || ! wcs_order_contains_subscription( $order, 'parent' ) || wcs_order_contains_subscription( $order, 'resubscribe' ) ) { + return; + } - if ( ! is_user_logged_in() ) { + if ( ! is_user_logged_in() ) { + // Allow the customer to login first and then redirect them back. + $redirect = add_query_arg( array( + 'wcs_redirect' => 'pay_for_order', + 'wcs_redirect_id' => $order_id, + ), get_permalink( wc_get_page_id( 'myaccount' ) ) ); + } elseif ( ! current_user_can( 'pay_for_order', $order_id ) ) { + wc_add_notice( __( 'That doesn\'t appear to be your order.', 'woocommerce-subscriptions' ), 'error' ); - $redirect = add_query_arg( array( - 'wcs_redirect' => 'pay_for_order', - 'wcs_redirect_id' => $order_id, - ), get_permalink( wc_get_page_id( 'myaccount' ) ) ); + $redirect = get_permalink( wc_get_page_id( 'myaccount' ) ); + } else { + $subscriptions = wcs_get_subscriptions_for_order( $order ); + do_action( 'wcs_before_parent_order_setup_cart', $subscriptions, $order ); - wp_safe_redirect( $redirect ); - exit; + // Add the existing order items to the cart + $this->setup_cart( $order, array( + 'order_id' => $order_id, + ) ); - } elseif ( ! current_user_can( 'pay_for_order', $order_id ) ) { + do_action( 'wcs_after_parent_order_setup_cart', $subscriptions, $order ); - wc_add_notice( __( 'That doesn\'t appear to be your order.', 'woocommerce-subscriptions' ), 'error' ); + // Store order's ID in the session so it can be re-used after payment + WC()->session->set( 'order_awaiting_payment', $order_id ); - wp_safe_redirect( get_permalink( wc_get_page_id( 'myaccount' ) ) ); - exit; + $this->set_cart_hash( $order_id ); - } else { + $redirect = wc_get_checkout_url(); + } - // Setup cart with all the original order's line items - $this->setup_cart( $order, array( - 'order_id' => $order_id, - ) ); - - WC()->session->set( 'order_awaiting_payment', $order_id ); - - // Set cart hash for orders paid in WC >= 2.6 - $this->set_cart_hash( $order_id ); - - wp_safe_redirect( wc_get_checkout_url() ); - exit; - } - } + if ( ! empty( $redirect ) ) { + wp_safe_redirect( $redirect ); + exit; } } @@ -124,4 +128,3 @@ class WCS_Cart_Initial_Payment extends WCS_Cart_Renewal { } } -new WCS_Cart_Initial_Payment(); diff --git a/includes/class-wcs-cart-renewal.php b/includes/class-wcs-cart-renewal.php old mode 100755 new mode 100644 index 4f252ae..6af40bb --- a/includes/class-wcs-cart-renewal.php +++ b/includes/class-wcs-cart-renewal.php @@ -54,6 +54,15 @@ class WCS_Cart_Renewal { // Remove subscription products with "one time shipping" from shipping packages. add_filter( 'woocommerce_cart_shipping_packages', array( $this, 'maybe_update_shipping_packages' ), 0, 1 ); + + add_action( 'wcs_before_renewal_setup_cart_subscriptions', array( &$this, 'clear_coupons' ), 10 ); + + // Handles renew of password-protected products. + add_action( 'wcs_before_renewal_setup_cart_subscriptions', 'wcs_allow_protected_products_to_renew' ); + add_action( 'wcs_after_renewal_setup_cart_subscriptions', 'wcs_disallow_protected_product_add_to_cart_validation' ); + + // Apply renewal discounts as pseudo coupons + add_action( 'woocommerce_setup_cart_for_subscription_renewal', array( $this, 'setup_discounts' ) ); } /** @@ -103,10 +112,7 @@ class WCS_Cart_Renewal { // Check if a user is requesting to create a renewal order for a subscription, needs to happen after $wp->query_vars are set add_action( 'template_redirect', array( &$this, 'maybe_setup_cart' ), 100 ); - // Apply renewal discounts as pseudo coupons - add_action( 'wcs_after_renewal_setup_cart_subscription', array( &$this, 'maybe_setup_discounts' ), 10, 2 ); add_filter( 'woocommerce_get_shop_coupon_data', array( &$this, 'renewal_coupon_data' ), 10, 2 ); - add_action( 'wcs_before_renewal_setup_cart_subscriptions', array( &$this, 'clear_coupons' ), 10 ); add_action( 'woocommerce_remove_cart_item', array( &$this, 'maybe_remove_items' ), 10, 1 ); add_action( 'woocommerce_before_cart_item_quantity_zero', array( &$this, 'maybe_remove_items' ), 10, 1 ); @@ -334,100 +340,6 @@ class WCS_Cart_Renewal { do_action( 'woocommerce_setup_cart_for_' . $this->cart_item_key, $subscription, $cart_item_data ); } - /** - * Check if a renewal order subscription has any coupons applied and if so add pseudo renewal coupon equivalents to ensure the discount is still applied - * - * @param WC_Subscription $subscription subscription - * @param WC_Order $order - * - * @since 2.0.10 - */ - public function maybe_setup_discounts( $subscription, $order = null ) { - if ( null === $order ) { - // If no order arg is passed, to honor backward compatibility, apply discounts which apply to the subscription - $order = $subscription; - } - - if ( wcs_is_subscription( $order ) || wcs_order_contains_renewal( $order ) ) { - - $used_coupons = $order->get_used_coupons(); - $order_discount = wcs_get_objects_property( $order, 'cart_discount' ); - - // Add any used coupon discounts to the cart (as best we can) using our pseudo renewal coupons - if ( ! empty( $used_coupons ) ) { - $coupon_items = $order->get_items( 'coupon' ); - - foreach ( $coupon_items as $coupon_item ) { - - $coupon = new WC_Coupon( $coupon_item['name'] ); - $coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' ); - $coupon_code = ''; - - // If the coupon still exists we can use the existing/available coupon properties - if ( true === wcs_get_coupon_property( $coupon, 'exists' ) ) { - - // But we only want to handle recurring coupons that have been applied to the order - if ( in_array( $coupon_type, array( 'recurring_percent', 'recurring_fee' ) ) ) { - - // Set the coupon type to be a renewal equivalent for correct validation and calculations - if ( 'recurring_percent' == $coupon_type ) { - wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_percent' ); - } elseif ( 'recurring_fee' == $coupon_type ) { - wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_fee' ); - } - - // Adjust coupon code to reflect that it is being applied to a renewal - $coupon_code = wcs_get_coupon_property( $coupon, 'code' ); - } - } else { - // If the coupon doesn't exist we can only really apply the discount amount we know about - so we'll apply a cart style pseudo coupon and then set the amount - wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_cart' ); - - // Adjust coupon code to reflect that it is being applied to a renewal - $coupon_code = wcs_get_coupon_property( $coupon, 'code' ); - $coupon_amount = is_callable( array( $coupon_item, 'get_discount' ) ) ? $coupon_item->get_discount() : $coupon_item['item_meta']['discount_amount']['0']; - - wcs_set_coupon_property( $coupon, 'coupon_amount', $coupon_amount ); - } - - // Now that we have a coupon we know we want to apply - if ( ! empty( $coupon_code ) ) { - - // Set renewal order products as the product ids on the coupon - wcs_set_coupon_property( $coupon, 'product_ids', $this->get_products( $order ) ); - - // Store the coupon info for later - $this->store_coupon( wcs_get_objects_property( $order, 'id' ), $coupon ); - - // Add the coupon to the cart - the actually coupon values / data are grabbed when needed later - if ( WC()->cart && ! WC()->cart->has_discount( $coupon_code ) ) { - WC()->cart->add_discount( $coupon_code ); - } - } - } - // If there are no coupons but there is still a discount (i.e. it might have been manually added), we need to account for that as well - } elseif ( ! empty( $order_discount ) ) { - $coupon = new WC_Coupon( 'discount_renewal' ); - - // Apply our cart style pseudo coupon and the set the amount - wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_cart' ); - - wcs_set_coupon_property( $coupon, 'coupon_amount', $order_discount ); - - // Set renewal order products as the product ids on the coupon - wcs_set_coupon_property( $coupon, 'product_ids', $this->get_products( $order ) ); - - // Store the coupon info for later - $this->store_coupon( wcs_get_objects_property( $order, 'id' ), $coupon ); - - // Add the coupon to the cart - if ( WC()->cart && ! WC()->cart->has_discount( 'discount_renewal' ) ) { - WC()->cart->add_discount( 'discount_renewal' ); - } - } - } - } - /** * Does some housekeeping. Fires after the items have been passed through the get items from session filter. Because * that filter is not good for removing cart items, we need to work around that by doing it later, in the cart @@ -1335,6 +1247,132 @@ class WCS_Cart_Renewal { return $packages; } + /** + * Check if the order has any discounts applied and if so reapply them to the cart + * or add pseudo coupon equivalents if the coupons no longer exist. + * + * @param WC_Order $order The order to copy coupons and discounts from. + * @since 2.4.3 + */ + public function setup_discounts( $order ) { + $order_discount = $order->get_total_discount(); + $coupon_items = $order->get_items( 'coupon' ); + + if ( empty( $order_discount ) && empty( $coupon_items ) ) { + return; + } + + $total_coupon_discount = floatval( array_sum( wc_list_pluck( $coupon_items, 'get_discount' ) ) ); + $coupons = array(); + + // If the order total discount is different from the discount applied from coupons we have a manually applied discount. + $order_has_manual_discount = $order_discount !== $total_coupon_discount; + + // Get all coupon line items as coupon objects. + if ( ! empty( $coupon_items ) ) { + $coupons = $this->get_line_item_coupons( $coupon_items ); + } + + if ( $order_has_manual_discount ) { + // Remove any coupon line items which don't grant free shipping. + foreach ( $coupons as $index => $coupon ) { + if ( ! $coupon->get_free_shipping() ) { + unset( $coupons[ $index ] ); + } + + // We're going to apply a coupon for the full order discount so make sure free shipping coupons don't apply any discount. + $coupon->set_amount( 0 ); + } + + $coupons[] = $this->get_pseudo_coupon( $order_discount ); + } + + foreach ( $coupons as $coupon ) { + $this->apply_order_coupon( $order, $coupon ); + } + } + + /** + * Create coupon objects from coupon line items. + * + * @param array $coupon_line_items The coupon line items to apply to the cart. + * @return array $coupons + */ + protected function get_line_item_coupons( $coupon_line_items ) { + $coupons = array(); + + foreach ( $coupon_line_items as $coupon_item ) { + $coupon = new WC_Coupon( $coupon_item['name'] ); + + // If the coupon no longer exists, get a pseudo coupon for the discounting amount. + if ( ! $coupon->get_id() > 0 ) { + // We shouldn't apply coupons which no longer exists it to initial payment carts. + if ( 'subscription_initial_payment' === $this->cart_item_key ) { + continue; + } + + $coupon = $this->get_pseudo_coupon( $coupon_item->get_discount() ); + $coupon->set_code( $coupon_item['code'] ); + } elseif ( 'subscription_renewal' === $this->cart_item_key ) { + $coupon_type = $coupon->get_discount_type(); + + // Change recurring coupons into renewal coupons so we can handle validation while paying for a renewal order manually. + if ( in_array( $coupon_type, array( 'recurring_percent', 'recurring_fee' ) ) ) { + $coupon->set_discount_type( str_replace( 'recurring', 'renewal', $coupon_type ) ); + } + } + + $coupons[] = $coupon; + } + + return $coupons; + } + + /** + * Apply a pseudo coupon to the cart for a specific discount amount. + * + * @param float $discount The discount amount. + * @return WC_Coupon + */ + protected function get_pseudo_coupon( $discount ) { + $cart_types = array( + 'subscription_initial_payment' => 'initial', + 'subscription_renewal' => 'renewal', + ); + + $cart_type = $cart_types[ $this->cart_item_key ]; + + // Generate a unique coupon code from the cart type. + $coupon = new WC_Coupon( "discount_{$cart_type}" ); + + // Apply our cart style pseudo coupon type and the set the amount. + $coupon->set_discount_type( "{$cart_type}_cart" ); + $coupon->set_amount( $discount ); + + return $coupon; + } + + /** + * Apply an order coupon to the cart. + * + * @param WC_Order $order The order the discount should apply to. + * @param WC_Coupon $coupon The coupon to add to the cart. + */ + protected function apply_order_coupon( $order, $coupon ) { + $coupon_code = $coupon->get_code(); + + // Set order products as the product ids on the coupon + $coupon->set_product_ids( $this->get_products( $order ) ); + + // Store the coupon info for later + $this->store_coupon( $order->get_id(), $coupon ); + + // Add the coupon to the cart + if ( WC()->cart && ! WC()->cart->has_discount( $coupon_code ) ) { + WC()->cart->add_discount( $coupon_code ); + } + } + /* Deprecated */ /** @@ -1413,5 +1451,101 @@ class WCS_Cart_Renewal { $this->update_cart_hash( $order ); return $order; } + + /** + * Check if a renewal order subscription has any coupons applied and if so add pseudo renewal coupon equivalents to ensure the discount is still applied + * + * @param WC_Subscription $subscription subscription + * @param WC_Order $order + * + * @since 2.0.10 + * @deprecated 2.4.3 + */ + public function maybe_setup_discounts( $subscription, $order = null ) { + wcs_deprecated_function( __METHOD__, '2.4.3' ); + + if ( null === $order ) { + // If no order arg is passed, to honor backward compatibility, apply discounts which apply to the subscription + $order = $subscription; + } + + if ( wcs_is_subscription( $order ) || wcs_order_contains_renewal( $order ) ) { + + $used_coupons = $order->get_used_coupons(); + $order_discount = wcs_get_objects_property( $order, 'cart_discount' ); + + // Add any used coupon discounts to the cart (as best we can) using our pseudo renewal coupons + if ( ! empty( $used_coupons ) ) { + $coupon_items = $order->get_items( 'coupon' ); + + foreach ( $coupon_items as $coupon_item ) { + + $coupon = new WC_Coupon( $coupon_item['name'] ); + $coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' ); + $coupon_code = ''; + + // If the coupon still exists we can use the existing/available coupon properties + if ( true === wcs_get_coupon_property( $coupon, 'exists' ) ) { + + // But we only want to handle recurring coupons that have been applied to the order + if ( in_array( $coupon_type, array( 'recurring_percent', 'recurring_fee' ) ) ) { + + // Set the coupon type to be a renewal equivalent for correct validation and calculations + if ( 'recurring_percent' == $coupon_type ) { + wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_percent' ); + } elseif ( 'recurring_fee' == $coupon_type ) { + wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_fee' ); + } + + // Adjust coupon code to reflect that it is being applied to a renewal + $coupon_code = wcs_get_coupon_property( $coupon, 'code' ); + } + } else { + // If the coupon doesn't exist we can only really apply the discount amount we know about - so we'll apply a cart style pseudo coupon and then set the amount + wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_cart' ); + + // Adjust coupon code to reflect that it is being applied to a renewal + $coupon_code = wcs_get_coupon_property( $coupon, 'code' ); + $coupon_amount = is_callable( array( $coupon_item, 'get_discount' ) ) ? $coupon_item->get_discount() : $coupon_item['item_meta']['discount_amount']['0']; + + wcs_set_coupon_property( $coupon, 'coupon_amount', $coupon_amount ); + } + + // Now that we have a coupon we know we want to apply + if ( ! empty( $coupon_code ) ) { + + // Set renewal order products as the product ids on the coupon + wcs_set_coupon_property( $coupon, 'product_ids', $this->get_products( $order ) ); + + // Store the coupon info for later + $this->store_coupon( wcs_get_objects_property( $order, 'id' ), $coupon ); + + // Add the coupon to the cart - the actually coupon values / data are grabbed when needed later + if ( WC()->cart && ! WC()->cart->has_discount( $coupon_code ) ) { + WC()->cart->add_discount( $coupon_code ); + } + } + } + // If there are no coupons but there is still a discount (i.e. it might have been manually added), we need to account for that as well + } elseif ( ! empty( $order_discount ) ) { + $coupon = new WC_Coupon( 'discount_renewal' ); + + // Apply our cart style pseudo coupon and the set the amount + wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_cart' ); + + wcs_set_coupon_property( $coupon, 'coupon_amount', $order_discount ); + + // Set renewal order products as the product ids on the coupon + wcs_set_coupon_property( $coupon, 'product_ids', $this->get_products( $order ) ); + + // Store the coupon info for later + $this->store_coupon( wcs_get_objects_property( $order, 'id' ), $coupon ); + + // Add the coupon to the cart + if ( WC()->cart && ! WC()->cart->has_discount( 'discount_renewal' ) ) { + WC()->cart->add_discount( 'discount_renewal' ); + } + } + } + } } -new WCS_Cart_Renewal(); diff --git a/includes/class-wcs-cart-resubscribe.php b/includes/class-wcs-cart-resubscribe.php old mode 100755 new mode 100644 index 03eb393..00cc053 --- a/includes/class-wcs-cart-resubscribe.php +++ b/includes/class-wcs-cart-resubscribe.php @@ -323,4 +323,3 @@ class WCS_Cart_Resubscribe extends WCS_Cart_Renewal { } } } -new WCS_Cart_Resubscribe(); diff --git a/includes/class-wcs-cart-switch.php b/includes/class-wcs-cart-switch.php old mode 100755 new mode 100644 index 66bef80..78e257a --- a/includes/class-wcs-cart-switch.php +++ b/includes/class-wcs-cart-switch.php @@ -104,7 +104,7 @@ class WCS_Cart_Switch extends WCS_Cart_Renewal { } $order_item = wcs_get_order_item( $item_id, $order ); - $product = WC_Subscriptions::get_product( wcs_get_canonical_product_id( $order_item ) ); + $product = wc_get_product( wcs_get_canonical_product_id( $order_item ) ); $product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); $order_product_data = array( @@ -160,4 +160,3 @@ class WCS_Cart_Switch extends WCS_Cart_Renewal { } } } -new WCS_Cart_Switch(); diff --git a/includes/class-wcs-change-payment-method-admin.php b/includes/class-wcs-change-payment-method-admin.php old mode 100755 new mode 100644 diff --git a/includes/class-wcs-download-handler.php b/includes/class-wcs-download-handler.php old mode 100755 new mode 100644 index 1a51fae..8cd078b --- a/includes/class-wcs-download-handler.php +++ b/includes/class-wcs-download-handler.php @@ -32,6 +32,8 @@ class WCS_Download_Handler { add_action( 'woocommerce_process_shop_order_meta', __CLASS__ . '::repair_permission_data', 60, 1 ); + add_action( 'woocommerce_admin_created_subscription', array( __CLASS__, 'grant_download_permissions' ) ); + add_action( 'deleted_post', __CLASS__ . '::delete_subscription_permissions' ); add_action( 'woocommerce_process_product_file_download_paths', __CLASS__ . '::grant_new_file_product_permissions', 11, 3 ); @@ -184,6 +186,17 @@ class WCS_Download_Handler { ", $post_id, '0000-00-00 00:00:00' ) ); } + /** + * Gives customers access to downloadable products in a subscription. + * Hooked into 'woocommerce_admin_created_subscription' to grant permissions to admin created subscriptions. + * + * @param WC_Subscription $subscription + * @since 2.4.2 + */ + public static function grant_download_permissions( $subscription ) { + wc_downloadable_product_permissions( $subscription->get_id() ); + } + /** * Remove download permissions attached to a subscription when it is permenantly deleted. * @@ -247,4 +260,3 @@ class WCS_Download_Handler { } } } -WCS_Download_Handler::init(); diff --git a/includes/class-wcs-failed-scheduled-action-manager.php b/includes/class-wcs-failed-scheduled-action-manager.php old mode 100755 new mode 100644 index ea04239..d6748ef --- a/includes/class-wcs-failed-scheduled-action-manager.php +++ b/includes/class-wcs-failed-scheduled-action-manager.php @@ -37,10 +37,11 @@ class WCS_Failed_Scheduled_Action_Manager { /** * Constructor. * - * @param WC_Logger $logger The WC Logger instance. + * @param WC_Logger_Interface $logger The WC Logger instance. + * * @since 2.2.19 */ - public function __construct( WC_Logger $logger ) { + public function __construct( WC_Logger_Interface $logger ) { $this->logger = $logger; } @@ -121,24 +122,26 @@ class WCS_Failed_Scheduled_Action_Manager { $affected_subscription_events .= $separator . $action['type'] . ' for ' . $subject; $separator = "\n"; - }?> -
-

', - '', - '

', - '', - '

', - '' . wp_kses( $affected_subscription_events, array( 'a' => array( 'href' => array() ) ) ) . '', - 'failed-scheduled-actions', - '', - '
' . esc_html__( 'Ignore this error (not recommended!)', 'woocommerce-subscriptions' ) . ' ' . esc_html__( 'Open up a ticket now!', 'woocommerce-subscriptions' ) . '
' - );?> -

-
set_content_template( 'html-failed-scheduled-action-notice.php', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/admin/', array( + 'failed_scheduled_actions' => $failed_scheduled_actions, + 'affected_subscription_events' => $affected_subscription_events, + ) ); + $notice->set_actions( array( + array( + 'name' => __( 'Ignore this error (not recommended)', 'woocommerce-subscriptions' ), + 'url' => wp_nonce_url( add_query_arg( 'wcs_scheduled_action_timeout_error_notice', 'ignore' ), 'wcs_scheduled_action_timeout_error_notice', '_wcsnonce' ), + 'class' => 'button', + ), + array( + 'name' => __( 'Open a ticket', 'woocommerce-subscriptions' ), + 'url' => 'https://woocommerce.com/my-account/marketplace-ticket-form/', + 'class' => 'button button-primary', + ), + ) ); + $notice->display(); } /** diff --git a/includes/class-wcs-limiter.php b/includes/class-wcs-limiter.php old mode 100755 new mode 100644 index d721649..e88ddf6 --- a/includes/class-wcs-limiter.php +++ b/includes/class-wcs-limiter.php @@ -134,35 +134,61 @@ class WCS_Limiter { public static function is_purchasable_switch( $is_purchasable, $product ) { $product_key = wcs_get_canonical_product_id( $product ); + // Set an empty cache if one isn't set yet. if ( ! isset( self::$is_purchasable_cache[ $product_key ] ) ) { self::$is_purchasable_cache[ $product_key ] = array(); } - if ( ! isset( self::$is_purchasable_cache[ $product_key ]['switch'] ) ) { + // Exit early if we've already determined this product's purchasability via switching. + if ( isset( self::$is_purchasable_cache[ $product_key ]['switch'] ) ) { + return self::$is_purchasable_cache[ $product_key ]['switch']; + } - if ( false === $is_purchasable && wcs_is_product_switchable_type( $product ) && WC_Subscriptions_Product::is_subscription( $product->get_id() ) && 'no' != wcs_get_product_limitation( $product ) && is_user_logged_in() && wcs_user_has_subscription( 0, $product->get_id(), wcs_get_product_limitation( $product ) ) ) { + if ( true === $is_purchasable || ! is_user_logged_in() || ! wcs_is_product_switchable_type( $product ) || ! WC_Subscriptions_Product::is_subscription( $product->get_id() ) ) { + self::$is_purchasable_cache[ $product_key ]['switch'] = $is_purchasable; + return self::$is_purchasable_cache[ $product_key ]['switch']; + } - //Adding to cart - if ( isset( $_GET['switch-subscription'] ) ) { + $user_id = get_current_user_id(); + $product_limitation = wcs_get_product_limitation( $product ); + + if ( 'no' == $product_limitation || ! wcs_user_has_subscription( $user_id, $product->get_id(), wcs_get_product_limitation( $product ) ) ) { + self::$is_purchasable_cache[ $product_key ]['switch'] = $is_purchasable; + return self::$is_purchasable_cache[ $product_key ]['switch']; + } + + // Limited products are only purchasable while switching the subscription which contains that product so we need the customer's subscriptions to this product. + $subscriptions = wcs_get_subscriptions( array( + 'customer_id' => $user_id, + 'status' => $product_limitation, + 'product_id' => $product->get_id(), + ) ); + + // Adding to cart + if ( isset( $_GET['switch-subscription'] ) && array_key_exists( $_GET['switch-subscription'], $subscriptions ) ) { + $is_purchasable = true; + } else { + // If we have a variation product get the variable product's ID. We can't use the variation ID for comparison because this function sometimes receives a variable product. + $product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); + $cart_contents = array(); + + // Use the version of the cart we have access to. We may need to look for switches in the cart being loaded from the session. + if ( WC_Subscriptions_Switcher::cart_contains_switches() ) { + $cart_contents = WC()->cart->cart_contents; + } elseif ( isset( WC()->session->cart ) ) { + $cart_contents = WC()->session->cart; + } + + // Check if the cart contains a switch for this specific product. + foreach ( $cart_contents as $cart_item ) { + if ( $product_id === $cart_item['product_id'] && isset( $cart_item['subscription_switch']['subscription_id'] ) && array_key_exists( $cart_item['subscription_switch']['subscription_id'], $subscriptions ) ) { $is_purchasable = true; - - //Validating when restoring cart from session - } elseif ( WC_Subscriptions_Switcher::cart_contains_switches() ) { - $is_purchasable = true; - - // Restoring cart from session, so need to check the cart in the session (WC_Subscriptions_Switcher::cart_contains_subscription_switch() only checks the cart) - } elseif ( isset( WC()->session->cart ) ) { - - foreach ( WC()->session->cart as $cart_item_key => $cart_item ) { - if ( $product->get_id() == $cart_item['product_id'] && isset( $cart_item['subscription_switch'] ) ) { - $is_purchasable = true; - break; - } - } + break; } } - self::$is_purchasable_cache[ $product_key ]['switch'] = $is_purchasable; } + + self::$is_purchasable_cache[ $product_key ]['switch'] = $is_purchasable; return self::$is_purchasable_cache[ $product_key ]['switch']; } @@ -246,4 +272,3 @@ class WCS_Limiter { } } -WCS_Limiter::init(); diff --git a/includes/class-wcs-my-account-payment-methods.php b/includes/class-wcs-my-account-payment-methods.php old mode 100755 new mode 100644 index abf915b..2c4a815 --- a/includes/class-wcs-my-account-payment-methods.php +++ b/includes/class-wcs-my-account-payment-methods.php @@ -334,4 +334,3 @@ class WCS_My_Account_Payment_Methods { exit(); } } -WCS_My_Account_Payment_Methods::init(); diff --git a/includes/class-wcs-post-meta-cache-manager-many-to-one.php b/includes/class-wcs-post-meta-cache-manager-many-to-one.php old mode 100755 new mode 100644 diff --git a/includes/class-wcs-post-meta-cache-manager.php b/includes/class-wcs-post-meta-cache-manager.php old mode 100755 new mode 100644 diff --git a/includes/class-wcs-query.php b/includes/class-wcs-query.php old mode 100755 new mode 100644 index 56d3468..1af0db7 --- a/includes/class-wcs-query.php +++ b/includes/class-wcs-query.php @@ -301,4 +301,3 @@ class WCS_Query extends WC_Query { return array_merge( $query_vars, $this->query_vars ); } } -new WCS_Query(); diff --git a/includes/class-wcs-remove-item.php b/includes/class-wcs-remove-item.php old mode 100755 new mode 100644 index 7ce6eeb..f44ced6 --- a/includes/class-wcs-remove-item.php +++ b/includes/class-wcs-remove-item.php @@ -185,4 +185,3 @@ class WCS_Remove_Item { } } -WCS_Remove_Item::init(); diff --git a/includes/class-wcs-retry-manager.php b/includes/class-wcs-retry-manager.php old mode 100755 new mode 100644 index 6d13907..4036ded --- a/includes/class-wcs-retry-manager.php +++ b/includes/class-wcs-retry-manager.php @@ -2,13 +2,12 @@ /** * Manage the process of retrying a failed renewal payment that previously failed. * - * @package WooCommerce Subscriptions - * @subpackage WCS_Retry_Manager - * @category Class - * @author Prospress - * @since 2.1 + * @package WooCommerce Subscriptions + * @subpackage WCS_Retry_Manager + * @category Class + * @author Prospress + * @since 2.1 */ -require_once( 'payment-retry/class-wcs-retry-admin.php' ); class WCS_Retry_Manager { @@ -24,6 +23,20 @@ class WCS_Retry_Manager { /* property to store the instance of WCS_Retry_Admin */ protected static $admin; + /** + * Background updater to process retries from old store. + * + * @var WCS_Retry_Background_Migrator + */ + protected static $background_migrator; + + /** + * Our table maker instance. + * + * @var WCS_Table_Maker + */ + protected static $table_maker; + /** * Attach callbacks and set the retry rules * @@ -31,15 +44,13 @@ class WCS_Retry_Manager { * @since 2.1 */ public static function init() { - self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_retry'; self::$admin = new WCS_Retry_Admin( self::$setting_id ); if ( self::is_retry_enabled() ) { + WCS_Retry_Email::init(); - self::load_classes(); - - add_filter( 'init', array( self::store(), 'init' ) ); + add_action( 'init', array( __CLASS__, 'init_store' ) ); add_filter( 'woocommerce_valid_order_statuses_for_payment', __CLASS__ . '::check_order_statuses_for_payment', 10, 2 ); @@ -57,6 +68,15 @@ class WCS_Retry_Manager { add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' ); add_filter( 'woocommerce_subscriptions_is_failed_renewal_order', __CLASS__ . '::compare_order_and_retry_statuses', 10, 3 ); + + add_action( 'plugins_loaded', __CLASS__ . '::load_dependant_classes' ); + + add_action( 'woocommerce_subscriptions_before_upgrade', __CLASS__ . '::upgrade', 11, 2 ); + + if ( ! self::$table_maker ) { + self::$table_maker = new WCS_Retry_Table_Maker(); + add_action( 'init', array( self::$table_maker, 'register_tables' ), 0 ); + } } } @@ -71,7 +91,7 @@ class WCS_Retry_Manager { */ public static function check_order_statuses_for_payment( $statuses, $order ) { - $last_retry = self::store()->get_last_retry_for_order( $order ); + $last_retry = self::store()->get_last_retry_for_order( wcs_get_objects_property( $order, 'id' ) ); if ( $last_retry ) { $statuses[] = $last_retry->get_rule()->get_status_to_apply( 'order' ); $statuses = array_unique( $statuses ); @@ -89,29 +109,6 @@ class WCS_Retry_Manager { return (bool) apply_filters( 'wcs_is_retry_enabled', 'yes' == get_option( self::$setting_id, 'no' ) ); } - /** - * Load all the retry classes if the retry system is enabled - * - * @codeCoverageIgnore - * @since 2.1 - */ - protected static function load_classes() { - - require_once( 'abstracts/abstract-wcs-retry-store.php' ); - - require_once( 'payment-retry/class-wcs-retry.php' ); - - require_once( 'payment-retry/class-wcs-retry-rule.php' ); - - require_once( 'payment-retry/class-wcs-retry-rules.php' ); - - require_once( 'payment-retry/class-wcs-retry-post-store.php' ); - - require_once( 'payment-retry/class-wcs-retry-email.php' ); - - require_once( 'admin/meta-boxes/class-wcs-meta-box-payment-retries.php' ); - } - /** * Add a renewal retry date type to Subscriptions date types * @@ -170,7 +167,7 @@ class WCS_Retry_Manager { } foreach ( self::store()->get_retry_ids_for_order( $post_id ) as $retry_id ) { - wp_trash_post( $retry_id ); + self::store()->delete_retry( $retry_id ); } } } @@ -294,18 +291,39 @@ class WCS_Retry_Manager { // if both statuses are still the same or there no special status was applied and the order still needs payment (i.e. there has been no manual intervention), trigger the payment hook if ( $valid_order_status && $valid_subscription_status ) { + $unique_payment_methods = array(); $last_order->update_status( 'pending', _x( 'Subscription renewal payment retry:', 'used in order note as reason for why order status changed', 'woocommerce-subscriptions' ), true ); - // Make sure the subscription is on hold in case something goes wrong while trying to process renewal and in case gateways expect the subscription to be on-hold, which is normally the case with a renewal payment foreach ( $subscriptions as $subscription ) { + // Make sure the subscription is on hold in case something goes wrong while trying to process renewal and in case gateways expect the subscription to be on-hold, which is normally the case with a renewal payment $subscription->update_status( 'on-hold', _x( 'Subscription renewal payment retry:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) ); + + // Store a hash of the payment method and payment meta to determine if there's a single payment method being used. + $payment_meta_hash = md5( $subscription->get_payment_method() . json_encode( $subscription->get_payment_method_meta() ) ); + $unique_payment_methods[ $payment_meta_hash ] = 1; } - WC_Subscriptions_Payment_Gateways::trigger_gateway_renewal_payment_hook( $last_order ); + // Delete the payment method from the renewal order if the subscription has changed to manual renewal. + if ( wcs_order_contains_manual_subscription( $last_order, 'renewal' ) ) { + $last_order->set_payment_method( '' ); + $last_order->add_order_note( 'Renewal payment retry skipped - related subscription has changed to manual renewal.' ); - // Now that we've attempted to process the payment, refresh the order - $last_order = wc_get_order( wcs_get_objects_property( $last_order, 'id' ) ); + $last_order->save(); + } elseif ( 1 < count( $unique_payment_methods ) ) { + // Throw an exception if there is more than 1 unique payment method. + // This could only occur under circumstances where batch processing renewals has grouped unlike subscriptions. + throw new Exception( __( 'Payment retry attempted on renewal order with multiple related subscriptions with no payment method in common.', 'woocommerce-subscriptions' ) ); + } else { + // Before attempting to process payment, update the renewal order's payment method and meta to match the subscription's - in case it has changed. + wcs_copy_payment_method_to_order( $subscription, $last_order ); + $last_order->save(); + + WC_Subscriptions_Payment_Gateways::trigger_gateway_renewal_payment_hook( $last_order ); + + // Now that we've attempted to process the payment, refresh the order + $last_order = wc_get_order( wcs_get_objects_property( $last_order, 'id' ) ); + } // if the order still needs payment, payment failed if ( $last_order->needs_payment() ) { @@ -344,25 +362,65 @@ class WCS_Retry_Manager { } /** - * Access the object used to interface with the database + * Loads/init our depended classes. * - * @since 2.1 + * @since 2.4 + */ + public static function load_dependant_classes() { + if ( ! self::$background_migrator ) { + self::$background_migrator = new WCS_Retry_Background_Migrator( wc_get_logger() ); + add_action( 'init', array( self::$background_migrator, 'init' ), 15 ); + } + } + + /** + * Runs our upgrade background scripts. + * + * @param string $new_version Version we're upgrading to. + * @param string $old_version Version we're upgrading from. + * + * @since 2.4 + */ + public static function upgrade( $new_version, $old_version ) { + if ( '0' !== $old_version && version_compare( $old_version, '2.4', '<' ) ) { + self::$background_migrator->schedule_repair(); + } + + if ( version_compare( $new_version, '2.4.0', '>' ) ) { + WCS_Retry_Migrator::set_needs_migration(); + } + } + + /** + * Access the object used to interface with the store. + * + * @since 2.4 */ public static function store() { if ( empty( self::$store ) ) { - $class = self::get_store_class(); + if ( ! did_action( 'plugins_loaded' ) ) { + wcs_doing_it_wrong( __METHOD__, 'This method was called before the "plugins_loaded" hook. It applies a filter to the retry data store instantiated. For that to work, it should first be called after all plugins are loaded.', '2.4.1' ); + } + + $class = self::get_store_class(); self::$store = new $class(); } + return self::$store; } /** * Get the class used for instantiating retry storage via self::store() * - * @since 2.1 + * @since 2.4 */ protected static function get_store_class() { - return apply_filters( 'wcs_retry_store_class', 'WCS_Retry_Post_Store' ); + $default_store_class = 'WCS_Retry_Database_Store'; + if ( WCS_Retry_Migrator::needs_migration() ) { + $default_store_class = 'WCS_Retry_Hybrid_Store'; + } + + return apply_filters( 'wcs_retry_store_class', $default_store_class ); } /** @@ -386,5 +444,16 @@ class WCS_Retry_Manager { protected static function get_rules_class() { return apply_filters( 'wcs_retry_rules_class', 'WCS_Retry_Rules' ); } + + /** + * Initialise the store object used to interface with retry data. + * + * Hooked onto 'init' to allow third-parties to use their own data store + * and to ensure WordPress is fully loaded. + * + * @since 2.4.1 + */ + public static function init_store() { + self::store()->init(); + } } -WCS_Retry_Manager::init(); diff --git a/includes/class-wcs-select2.php b/includes/class-wcs-select2.php old mode 100755 new mode 100644 diff --git a/includes/class-wcs-staging.php b/includes/class-wcs-staging.php old mode 100755 new mode 100644 diff --git a/includes/class-wcs-template-loader.php b/includes/class-wcs-template-loader.php old mode 100755 new mode 100644 index c648687..b23ae35 --- a/includes/class-wcs-template-loader.php +++ b/includes/class-wcs-template-loader.php @@ -63,4 +63,3 @@ class WCS_Template_Loader { wc_get_template( 'myaccount/subscription-totals.php', array( 'subscription' => $subscription ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); } } -WCS_Template_Loader::init(); diff --git a/includes/class-wcs-user-change-status-handler.php b/includes/class-wcs-user-change-status-handler.php old mode 100755 new mode 100644 index 47f75ab..e1f8c7e --- a/includes/class-wcs-user-change-status-handler.php +++ b/includes/class-wcs-user-change-status-handler.php @@ -112,4 +112,3 @@ class WCS_User_Change_Status_Handler { return true; } } -WCS_User_Change_Status_Handler::init(); diff --git a/includes/class-wcs-webhooks.php b/includes/class-wcs-webhooks.php old mode 100755 new mode 100644 index c1c2275..e0ed2dd --- a/includes/class-wcs-webhooks.php +++ b/includes/class-wcs-webhooks.php @@ -139,8 +139,6 @@ class WCS_Webhooks { break; case 'wp_api_v1': case 'wp_api_v2': - require_once( 'api/class-wc-rest-subscriptions-controller.php' ); - $request = new WP_REST_Request( 'GET' ); $controller = new WC_REST_Subscriptions_Controller; @@ -214,4 +212,3 @@ class WCS_Webhooks { } } -WCS_Webhooks::init(); diff --git a/includes/data-stores/class-wcs-customer-store-cached-cpt.php b/includes/data-stores/class-wcs-customer-store-cached-cpt.php old mode 100755 new mode 100644 index af67207..06cb521 --- a/includes/data-stores/class-wcs-customer-store-cached-cpt.php +++ b/includes/data-stores/class-wcs-customer-store-cached-cpt.php @@ -81,13 +81,15 @@ class WCS_Customer_Store_Cached_CPT extends WCS_Customer_Store_CPT implements WC if ( false === $subscription_ids ) { $subscription_ids = parent::get_users_subscription_ids( $user_id ); // no data in transient, query directly } else { - rsort( $subscription_ids ); // the results from the database query are ordered by date/ID in DESC, so make sure the transient value is too delete_transient( $transient_key ); // migrate the data to our new cache } $this->update_subscription_id_cache( $user_id, $subscription_ids ); } + // Sort results in order to keep consistency between cached results and queried results. + rsort( $subscription_ids ); + return $subscription_ids; } @@ -156,6 +158,7 @@ class WCS_Customer_Store_Cached_CPT extends WCS_Customer_Store_CPT implements WC return false; } + rsort( $subscription_ids ); // the results from the database query are ordered by date/ID in DESC, so make sure the user cached values are ordered the same. return update_user_meta( $user_id, $this->cache_meta_key, $subscription_ids ); } @@ -168,6 +171,19 @@ class WCS_Customer_Store_Cached_CPT extends WCS_Customer_Store_CPT implements WC delete_metadata( 'user', null, $this->cache_meta_key, null, true ); } + /** + * Clears the cache for a given user. + * + * @param int $user_id The id of the user + */ + public function delete_cache_for_user( $user_id ) { + if ( empty( $user_id ) ) { + return; + } + + delete_user_meta( $user_id, $this->cache_meta_key ); + } + /* Public methods used as callbacks on hooks for managing cache */ /** diff --git a/includes/data-stores/class-wcs-customer-store-cpt.php b/includes/data-stores/class-wcs-customer-store-cpt.php old mode 100755 new mode 100644 diff --git a/includes/data-stores/class-wcs-product-variable-data-store-cpt.php b/includes/data-stores/class-wcs-product-variable-data-store-cpt.php old mode 100755 new mode 100644 diff --git a/includes/data-stores/class-wcs-related-order-store-cached-cpt.php b/includes/data-stores/class-wcs-related-order-store-cached-cpt.php old mode 100755 new mode 100644 diff --git a/includes/data-stores/class-wcs-related-order-store-cpt.php b/includes/data-stores/class-wcs-related-order-store-cpt.php old mode 100755 new mode 100644 diff --git a/includes/data-stores/class-wcs-subscription-data-store-cpt.php b/includes/data-stores/class-wcs-subscription-data-store-cpt.php old mode 100755 new mode 100644 index 52f6cde..21c8a27 --- a/includes/data-stores/class-wcs-subscription-data-store-cpt.php +++ b/includes/data-stores/class-wcs-subscription-data-store-cpt.php @@ -31,6 +31,7 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements '_schedule_end', '_schedule_payment_retry', '_subscription_switch_data', + '_schedule_start', ); /** @@ -54,6 +55,7 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements '_schedule_cancelled' => 'schedule_cancelled', '_schedule_end' => 'schedule_end', '_schedule_payment_retry' => 'schedule_payment_retry', + '_schedule_start' => 'schedule_start', '_subscription_switch_data' => 'switch_data', ); @@ -62,6 +64,25 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements * Constructor. */ public function __construct() { + + // Register any custom date types as internal meta keys and props. + foreach ( wcs_get_subscription_date_types() as $date_type => $date_name ) { + // The last payment date is derived from other sources and shouldn't be stored on a subscription. + if ( 'last_payment' === $date_type ) { + continue; + } + + $meta_key = wcs_get_date_meta_key( $date_type ); + + // Skip any dates which are already core date types. We don't want custom date types to override them. + if ( isset( $this->subscription_meta_keys_to_props[ $meta_key ] ) ) { + continue; + } + + $this->subscription_meta_keys_to_props[ $meta_key ] = wcs_maybe_prefix_key( $date_type, 'schedule_' ); + $this->subscription_internal_meta_keys[] = $meta_key; + } + // Exclude the subscription related meta data we set and manage manually from the objects "meta" data $this->internal_meta_keys = array_merge( $this->internal_meta_keys, $this->subscription_internal_meta_keys ); } @@ -122,6 +143,11 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements // Dates are set via update_dates() to make sure relationships between dates are validated if ( 0 === strpos( $prop_key, 'schedule' ) ) { $date_type = str_replace( 'schedule_', '', $prop_key ); + + if ( 'start' === $date_type && ! $meta_value ) { + $meta_value = $subscription->get_date( 'date_created' ); + } + $dates_to_set[ $date_type ] = ( false == $meta_value ) ? 0 : $meta_value; } else { $props_to_set[ $prop_key ] = $meta_value; @@ -129,6 +155,23 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements } } + // On WC 3.5.0 the ID of the user that placed the order was moved from the post meta _customer_user to the post_author field in the wp_posts table. + // If the update routine didn't manage to cover subscriptions, we need to use the value stored as post meta until our own update finishes. + if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '>=' ) && 1 == $post_object->post_author && get_option( 'wcs_subscription_post_author_upgrade_is_scheduled', false ) ) { + $props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true ); + } else { + /** + * WC 3.5.0 and our 2.4.0 post author upgrade scripts didn't account for subscriptions created manually by admin users with a user ID not equal to 1. + * This resulted in those subscription post author columns not being updated and so linked to the admin user who created them, not the customer. + * + * Until a permanent fix is found, revert to the previous behavior of getting the customer user from post meta. + * This will be eventually removed. + * + * @see https://github.com/Prospress/woocommerce-subscriptions/issues/3036 + */ + $props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true ); + } + $subscription->update_dates( $dates_to_set ); $subscription->set_props( $props_to_set ); } @@ -305,8 +348,22 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements '_schedule_cancelled', '_schedule_end', '_schedule_payment_retry', + '_schedule_start', ); + // Add any custom date types to the date meta keys we need to save. + foreach ( wcs_get_subscription_date_types() as $date_type => $date_name ) { + if ( 'last_payment' === $date_type ) { + continue; + } + + $date_meta_key = wcs_get_date_meta_key( $date_type ); + + if ( ! in_array( $date_meta_key, $date_meta_keys ) ) { + $date_meta_keys[] = $date_meta_key; + } + } + $date_meta_keys_to_props = array_intersect_key( $this->subscription_meta_keys_to_props, array_flip( $date_meta_keys ) ); // Save the changes to scheduled dates diff --git a/includes/deprecated/class-wcs-action-deprecator.php b/includes/deprecated/class-wcs-action-deprecator.php old mode 100755 new mode 100644 index 9f27ec8..5a94fe1 --- a/includes/deprecated/class-wcs-action-deprecator.php +++ b/includes/deprecated/class-wcs-action-deprecator.php @@ -125,4 +125,3 @@ class WCS_Action_Deprecator extends WCS_Hook_Deprecator { } } } -new WCS_Action_Deprecator(); diff --git a/includes/deprecated/class-wcs-deprecated-filter-hooks.php b/includes/deprecated/class-wcs-deprecated-filter-hooks.php old mode 100755 new mode 100644 index 051f2b4..75c292c --- a/includes/deprecated/class-wcs-deprecated-filter-hooks.php +++ b/includes/deprecated/class-wcs-deprecated-filter-hooks.php @@ -52,4 +52,3 @@ class WCS_Deprecated_Filter_Hooks extends WC_Deprecated_Filter_Hooks { wcs_deprecated_function( sprintf( 'The "%s" hook uses out of date data structures and', esc_html( $old_hook ) ), '2.2.0', esc_html( $new_hook ) . ' to filter subscription properties' ); } } -new WCS_Deprecated_Filter_Hooks(); diff --git a/includes/deprecated/class-wcs-dynamic-action-deprecator.php b/includes/deprecated/class-wcs-dynamic-action-deprecator.php old mode 100755 new mode 100644 index 1ed5b84..7497a54 --- a/includes/deprecated/class-wcs-dynamic-action-deprecator.php +++ b/includes/deprecated/class-wcs-dynamic-action-deprecator.php @@ -106,4 +106,3 @@ class WCS_Dynamic_Action_Deprecator extends WCS_Dynamic_Hook_Deprecator { } } } -new WCS_Dynamic_Action_Deprecator(); diff --git a/includes/deprecated/class-wcs-dynamic-filter-deprecator.php b/includes/deprecated/class-wcs-dynamic-filter-deprecator.php old mode 100755 new mode 100644 index cf435d2..0627ea5 --- a/includes/deprecated/class-wcs-dynamic-filter-deprecator.php +++ b/includes/deprecated/class-wcs-dynamic-filter-deprecator.php @@ -48,4 +48,3 @@ class WCS_Dynamic_Filter_Deprecator extends WCS_Dynamic_Hook_Deprecator { return $return_value; } } -new WCS_Dynamic_Filter_Deprecator(); diff --git a/includes/deprecated/class-wcs-filter-deprecator.php b/includes/deprecated/class-wcs-filter-deprecator.php old mode 100755 new mode 100644 index e5354e7..6381bce --- a/includes/deprecated/class-wcs-filter-deprecator.php +++ b/includes/deprecated/class-wcs-filter-deprecator.php @@ -24,9 +24,7 @@ class WCS_Filter_Deprecator extends WCS_Hook_Deprecator { 'woocommerce_subscription_payment_completed_count' => 'woocommerce_subscription_completed_payment_count', 'woocommerce_subscription_get_end_date' => 'woocommerce_subscription_expiration_date', 'woocommerce_subscription_get_trial_end_date' => 'woocommerce_subscription_trial_expiration_date', - 'woocommerce_subscription_date_updated' => 'woocommerce_subscriptions_set_expiration_date', 'woocommerce_subscriptions_product_expiration_date' => 'woocommerce_subscription_calculated_expiration_date', - 'woocommerce_subscription_date_updated' => 'woocommerce_subscription_set_next_payment_date', 'woocommerce_subscription_get_last_payment_date' => 'woocommerce_subscription_last_payment_date', 'woocommerce_subscription_calculated_next_payment_date' => 'woocommerce_subscriptions_calculated_next_payment_date', 'woocommerce_subscription_date_updated' => 'woocommerce_subscriptions_set_trial_expiration_date', @@ -335,4 +333,3 @@ class WCS_Filter_Deprecator extends WCS_Hook_Deprecator { return $return_value; } } -new WCS_Filter_Deprecator(); diff --git a/includes/early-renewal/class-wcs-cart-early-renewal.php b/includes/early-renewal/class-wcs-cart-early-renewal.php old mode 100755 new mode 100644 index 4c62a61..a8889d5 --- a/includes/early-renewal/class-wcs-cart-early-renewal.php +++ b/includes/early-renewal/class-wcs-cart-early-renewal.php @@ -47,6 +47,10 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { // Allow customers to cancel early renewal orders from their my account page. add_filter( 'woocommerce_my_account_my_orders_actions', array( $this, 'add_cancel_order_action' ), 15, 2 ); add_action( 'wp_loaded', array( $this, 'allow_early_renewal_order_cancellation' ), 10, 3 ); + + // Handles early renew of password-protected products. + add_action( 'wcs_before_early_renewal_setup_cart_subscription', 'wcs_allow_protected_products_to_renew' ); + add_action( 'wcs_after_early_renewal_setup_cart_subscription', 'wcs_disallow_protected_product_add_to_cart_validation' ); } /** @@ -74,20 +78,14 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { * Check if a payment is being made on an early renewal order. */ public function maybe_setup_cart() { - global $wp; - - if ( ! isset( $_GET['subscription_renewal_early'], $_GET['wcs_nonce'] ) ) { + if ( ! isset( $_GET['subscription_renewal_early'] ) ) { return; } $subscription = wcs_get_subscription( absint( $_GET['subscription_renewal_early'] ) ); $redirect_to = get_permalink( wc_get_page_id( 'myaccount' ) ); - if ( false === wp_verify_nonce( $_GET['wcs_nonce'], 'wcs-renew-' . $subscription->get_id() ) ) { - - wc_add_notice( __( 'There was an error with your request to renew. Please try again.', 'woocommerce-subscriptions' ), 'error' ); - - } elseif ( empty( $subscription ) ) { + if ( empty( $subscription ) ) { wc_add_notice( __( 'That subscription does not exist. Has it been deleted?', 'woocommerce-subscriptions' ), 'error' ); @@ -101,6 +99,8 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { } else { + do_action( 'wcs_before_early_renewal_setup_cart_subscription', $subscription ); + wc_add_notice( __( 'Complete checkout to renew now.', 'woocommerce-subscriptions' ), 'success' ); $this->setup_cart( $subscription, array( @@ -109,6 +109,8 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal { 'renewal_order_id' => $subscription->get_id(), ) ); + do_action( 'wcs_after_early_renewal_setup_cart_subscription', $subscription ); + $redirect_to = wc_get_checkout_url(); } diff --git a/includes/early-renewal/class-wcs-early-renewal-manager.php b/includes/early-renewal/class-wcs-early-renewal-manager.php old mode 100755 new mode 100644 diff --git a/includes/early-renewal/wcs-early-renewal-functions.php b/includes/early-renewal/wcs-early-renewal-functions.php old mode 100755 new mode 100644 index 0c97733..602f784 --- a/includes/early-renewal/wcs-early-renewal-functions.php +++ b/includes/early-renewal/wcs-early-renewal-functions.php @@ -47,7 +47,7 @@ function wcs_can_user_renew_early( $subscription, $user_id = 0 ) { $reason = 'not_a_subscription'; } elseif ( ! $subscription->has_status( array( 'active' ) ) ) { $reason = 'subscription_not_active'; - } elseif ( 0.0 === $subscription->get_total() ) { + } elseif ( 0.0 === floatval( $subscription->get_total() ) ) { $reason = 'subscription_zero_total'; } elseif ( $subscription->get_time( 'trial_end' ) > gmdate( 'U' ) ) { $reason = 'subscription_still_in_free_trial'; @@ -134,7 +134,6 @@ function wcs_get_early_renewal_url( $subscription ) { $url = add_query_arg( array( 'subscription_renewal_early' => $subscription_id, 'subscription_renewal' => 'true', - 'wcs_nonce' => wp_create_nonce( 'wcs-renew-' . $subscription_id ), ), get_permalink( wc_get_page_id( 'myaccount' ) ) ); /** diff --git a/includes/emails/class-wcs-email-cancelled-subscription.php b/includes/emails/class-wcs-email-cancelled-subscription.php old mode 100755 new mode 100644 index e9827ed..0a7b5c8 --- a/includes/emails/class-wcs-email-cancelled-subscription.php +++ b/includes/emails/class-wcs-email-cancelled-subscription.php @@ -19,7 +19,6 @@ class WCS_Email_Cancelled_Subscription extends WC_Email { * Create an instance of the class. * * @access public - * @return void */ function __construct() { diff --git a/includes/emails/class-wcs-email-customer-completed-renewal-order.php b/includes/emails/class-wcs-email-completed-renewal-order.php old mode 100755 new mode 100644 similarity index 100% rename from includes/emails/class-wcs-email-customer-completed-renewal-order.php rename to includes/emails/class-wcs-email-completed-renewal-order.php diff --git a/includes/emails/class-wcs-email-customer-completed-switch-order.php b/includes/emails/class-wcs-email-completed-switch-order.php old mode 100755 new mode 100644 similarity index 100% rename from includes/emails/class-wcs-email-customer-completed-switch-order.php rename to includes/emails/class-wcs-email-completed-switch-order.php diff --git a/includes/emails/class-wcs-email-customer-payment-retry.php b/includes/emails/class-wcs-email-customer-payment-retry.php old mode 100755 new mode 100644 diff --git a/includes/emails/class-wcs-email-customer-renewal-invoice.php b/includes/emails/class-wcs-email-customer-renewal-invoice.php old mode 100755 new mode 100644 diff --git a/includes/emails/class-wcs-email-expired-subscription.php b/includes/emails/class-wcs-email-expired-subscription.php old mode 100755 new mode 100644 index 47cecfe..d1c80d3 --- a/includes/emails/class-wcs-email-expired-subscription.php +++ b/includes/emails/class-wcs-email-expired-subscription.php @@ -19,7 +19,6 @@ class WCS_Email_Expired_Subscription extends WC_Email { * Create an instance of the class. * * @access public - * @return void */ function __construct() { diff --git a/includes/emails/class-wcs-email-new-renewal-order.php b/includes/emails/class-wcs-email-new-renewal-order.php old mode 100755 new mode 100644 diff --git a/includes/emails/class-wcs-email-new-switch-order.php b/includes/emails/class-wcs-email-new-switch-order.php old mode 100755 new mode 100644 diff --git a/includes/emails/class-wcs-email-on-hold-subscription.php b/includes/emails/class-wcs-email-on-hold-subscription.php old mode 100755 new mode 100644 index 26bf84d..d42f9e1 --- a/includes/emails/class-wcs-email-on-hold-subscription.php +++ b/includes/emails/class-wcs-email-on-hold-subscription.php @@ -19,7 +19,6 @@ class WCS_Email_On_Hold_Subscription extends WC_Email { * Create an instance of the class. * * @access public - * @return void */ function __construct() { diff --git a/includes/emails/class-wcs-email-payment-retry.php b/includes/emails/class-wcs-email-payment-retry.php old mode 100755 new mode 100644 diff --git a/includes/emails/class-wcs-email-customer-processing-renewal-order.php b/includes/emails/class-wcs-email-processing-renewal-order.php old mode 100755 new mode 100644 similarity index 100% rename from includes/emails/class-wcs-email-customer-processing-renewal-order.php rename to includes/emails/class-wcs-email-processing-renewal-order.php diff --git a/includes/gateways/class-wc-subscriptions-payment-gateways.php b/includes/gateways/class-wc-subscriptions-payment-gateways.php old mode 100755 new mode 100644 index 55100f5..d364984 --- a/includes/gateways/class-wc-subscriptions-payment-gateways.php +++ b/includes/gateways/class-wc-subscriptions-payment-gateways.php @@ -40,7 +40,6 @@ class WC_Subscriptions_Payment_Gateways { * @since 2.0 */ public static function init_paypal() { - require_once( 'paypal/class-wcs-paypal.php' ); WCS_PayPal::init(); } @@ -265,5 +264,3 @@ class WC_Subscriptions_Payment_Gateways { self::trigger_gateway_status_updated_hook( wcs_get_subscription_from_key( $subscription_key ), 'expired' ); } } - -WC_Subscriptions_Payment_Gateways::init(); diff --git a/includes/gateways/paypal/class-wcs-paypal.php b/includes/gateways/paypal/class-wcs-paypal.php old mode 100755 new mode 100644 index 9400dc4..250b1da --- a/includes/gateways/paypal/class-wcs-paypal.php +++ b/includes/gateways/paypal/class-wcs-paypal.php @@ -16,17 +16,6 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -require_once( 'includes/wcs-paypal-functions.php' ); -require_once( 'includes/class-wcs-paypal-supports.php' ); -require_once( 'includes/class-wcs-paypal-status-manager.php' ); -require_once( 'includes/class-wcs-paypal-standard-switcher.php' ); -require_once( 'includes/class-wcs-paypal-standard-request.php' ); -require_once( 'includes/class-wcs-paypal-standard-change-payment-method.php' ); -require_once( 'includes/admin/class-wcs-paypal-admin.php' ); -require_once( 'includes/admin/class-wcs-paypal-change-payment-method-admin.php' ); -require_once( 'includes/deprecated/class-wc-paypal-standard-subscriptions.php' ); -require_once( 'includes/class-wcs-paypal-standard-ipn-failure-handler.php' ); - class WCS_PayPal { /** @var WCS_PayPal_Express_API for communicating with PayPal */ @@ -308,9 +297,6 @@ class WCS_PayPal { public static function process_ipn_request( $transaction_details ) { try { - require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' ); - require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' ); - if ( ! isset( $transaction_details['txn_type'] ) || ! in_array( $transaction_details['txn_type'], array_merge( self::get_ipn_handler( 'standard' )->get_transaction_types(), self::get_ipn_handler( 'reference' )->get_transaction_types() ) ) ) { return; } @@ -468,7 +454,6 @@ class WCS_PayPal { if ( 'reference' === $ipn_type ) { if ( ! isset( self::$ipn_handlers['reference'] ) ) { - require_once( 'includes/class-wcs-paypal-reference-transaction-ipn-handler.php' ); self::$ipn_handlers['reference'] = new WCS_Paypal_Reference_Transaction_IPN_Handler( $use_sandbox, self::get_option( 'receiver_email' ) ); } @@ -477,7 +462,6 @@ class WCS_PayPal { } else { if ( ! isset( self::$ipn_handlers['standard'] ) ) { - require_once( 'includes/class-wcs-paypal-standard-ipn-handler.php' ); self::$ipn_handlers['standard'] = new WCS_Paypal_Standard_IPN_Handler( $use_sandbox, self::get_option( 'receiver_email' ) ); } @@ -504,20 +488,6 @@ class WCS_PayPal { require_once( WC()->plugin_path() . '/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php' ); } - $classes = array( - 'api', - 'api-request', - 'api-response', - 'api-response-checkout', - 'api-response-billing-agreement', - 'api-response-payment', - 'api-response-recurring-payment', - ); - - foreach ( $classes as $class ) { - require_once( "includes/class-wcs-paypal-reference-transaction-{$class}.php" ); - } - $environment = ( 'yes' === self::get_option( 'testmode' ) ) ? 'sandbox' : 'production'; return self::$api = new WCS_PayPal_Reference_Transaction_API( 'paypal', $environment, self::get_option( 'api_username' ), self::get_option( 'api_password' ), self::get_option( 'api_signature' ) ); diff --git a/includes/gateways/paypal/includes/abstracts/abstract-wcs-sv-api-base.php b/includes/gateways/paypal/includes/abstracts/abstract-wcs-sv-api-base.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php old mode 100755 new mode 100644 index 662f3ad..b6f4b5c --- a/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php +++ b/includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php @@ -87,110 +87,106 @@ class WCS_PayPal_Admin { * @since 2.0 */ public static function maybe_show_admin_notices() { - self::maybe_disable_invalid_profile_notice(); - if ( ! in_array( get_woocommerce_currency(), apply_filters( 'woocommerce_paypal_supported_currencies', array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB' ) ) ) ) { - $valid_for_use = false; - } else { - $valid_for_use = true; + $valid_paypal_currency = in_array( get_woocommerce_currency(), apply_filters( 'woocommerce_paypal_supported_currencies', array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB' ) ) ); + + if ( ! $valid_paypal_currency || ! current_user_can( 'manage_options' ) || has_action( 'admin_notices', 'WC_Subscriptions_Admin::admin_installed_notice' ) || 'yes' !== WCS_PayPal::get_option( 'enabled' ) ) { + return; } $payment_gateway_tab_url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=wc_gateway_paypal' ); + $notices = array(); - $notices = array(); + if ( ! WCS_PayPal::are_credentials_set() ) { + $notices[] = array( + 'type' => 'warning', + // translators: placeholders are opening and closing link tags. 1$-2$: to docs on woocommerce, 3$-4$ to gateway settings on the site + 'text' => sprintf( esc_html__( 'PayPal is inactive for subscription transactions. Please %1$sset up the PayPal IPN%2$s and %3$senter your API credentials%4$s to enable PayPal for Subscriptions.', 'woocommerce-subscriptions' ), + '', + '', + '', + '' + ), + ); + } elseif ( 'woocommerce_page_wc-settings' === get_current_screen()->base && isset( $_GET['tab'] ) && in_array( $_GET['tab'], array( 'subscriptions', 'checkout' ) ) && ! WCS_PayPal::are_reference_transactions_enabled() ) { + $notices[] = array( + 'type' => 'warning', + // translators: placeholders are opening and closing strong and link tags. 1$-2$: strong tags, 3$-8$ link to docs on woocommerce + 'text' => sprintf( esc_html__( '%1$sPayPal Reference Transactions are not enabled on your account%2$s, some subscription management features are not enabled. Please contact PayPal and request they %3$senable PayPal Reference Transactions%4$s on your account. %5$sCheck PayPal Account%6$s %3$sLearn more %7$s', 'woocommerce-subscriptions' ), + '', + '', + '', + '', + '

', + '', + '»' + ), + ); + } - if ( $valid_for_use && 'yes' == WCS_PayPal::get_option( 'enabled' ) && ! has_action( 'admin_notices', 'WC_Subscriptions_Admin::admin_installed_notice' ) && current_user_can( 'manage_options' ) ) { + if ( isset( $_GET['wcs_paypal'] ) && 'rt_enabled' === $_GET['wcs_paypal'] ) { + $notices[] = array( + 'type' => 'confirmation', + // translators: placeholders are opening and closing strong tags. + 'text' => sprintf( esc_html__( '%1$sPayPal Reference Transactions are enabled on your account%2$s. All subscription management features are now enabled. Happy selling!', 'woocommerce-subscriptions' ), + '', + '' + ), + ); + } - if ( ! WCS_PayPal::are_credentials_set() ) { + if ( false !== get_option( 'wcs_paypal_credentials_error' ) ) { + $notices[] = array( + 'type' => 'error', + // translators: placeholders are link opening and closing tags. 1$-2$: to gateway settings, 3$-4$: support docs on woocommerce.com + 'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your API credentials may be incorrect. Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s.', 'woocommerce-subscriptions' ), + '', + '', + '', + '' + ), + ); + } - $notices[] = array( - 'type' => 'warning', - // translators: placeholders are opening and closing link tags. 1$-2$: to docs on woocommerce, 3$-4$ to gateway settings on the site - 'text' => sprintf( esc_html__( 'PayPal is inactive for subscription transactions. Please %1$sset up the PayPal IPN%2$s and %3$senter your API credentials%4$s to enable PayPal for Subscriptions.', 'woocommerce-subscriptions' ), - '', - '', - '', - '' - ), - ); + if ( 'yes' == get_option( 'wcs_paypal_invalid_profile_id' ) ) { + $notices[] = array( + 'type' => 'error', + // translators: placeholders are opening and closing link tags. 1$-2$: docs on woocommerce, 3$-4$: dismiss link + 'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your PayPal account is issuing out-of-date subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s.', 'woocommerce-subscriptions' ), + '', + '', + '', + '' + ), + ); + } - } elseif ( 'woocommerce_page_wc-settings' === get_current_screen()->base && isset( $_GET['tab'] ) && in_array( $_GET['tab'], array( 'subscriptions', 'checkout' ) ) && ! WCS_PayPal::are_reference_transactions_enabled() ) { + $last_ipn_error = get_option( 'wcs_fatal_error_handling_ipn', '' ); + $failed_ipn_log_handle = 'wcs-ipn-failures'; - $notices[] = array( - 'type' => 'warning', - // translators: placeholders are opening and closing strong and link tags. 1$-2$: strong tags, 3$-8$ link to docs on woocommerce - 'text' => sprintf( esc_html__( '%1$sPayPal Reference Transactions are not enabled on your account%2$s, some subscription management features are not enabled. Please contact PayPal and request they %3$senable PayPal Reference Transactions%4$s on your account. %5$sCheck PayPal Account%6$s %3$sLearn more %7$s', 'woocommerce-subscriptions' ), - '', - '', - '', - '', - '

', - '', - '»' - ), - ); + if ( ! empty( $last_ipn_error ) && ( false == get_option( 'wcs_fatal_error_handling_ipn_ignored', false ) || isset( $_GET['wcs_reveal_your_ipn_secrets'] ) ) ) { + $notice = new WCS_Admin_Notice( 'error' ); + $notice->set_content_template( 'html-ipn-failure-notice.php', dirname( __FILE__ ) . '/../templates/', array( + 'failed_ipn_log_handle' => $failed_ipn_log_handle, + 'last_ipn_error' => $last_ipn_error, + 'log_file_url' => admin_url( sprintf( 'admin.php?page=wc-status&tab=logs&log_file=%s-%s-log', $failed_ipn_log_handle, sanitize_file_name( wp_hash( $failed_ipn_log_handle ) ) ) ), + ) ); - } + $notice->set_actions( array( + array( + 'name' => __( 'Ignore this error (not recommended)', 'woocommerce-subscriptions' ), + 'url' => wp_nonce_url( add_query_arg( 'wcs_ipn_error_notice', 'ignore' ), 'wcs_ipn_error_notice', '_wcsnonce' ), + 'class' => 'button', + ), + array( + 'name' => __( 'Open a ticket', 'woocommerce-subscriptions' ), + 'url' => 'https://woocommerce.com/my-account/marketplace-ticket-form/', + 'class' => 'button button-primary', + ), + ) ); - if ( isset( $_GET['wcs_paypal'] ) && 'rt_enabled' === $_GET['wcs_paypal'] ) { - $notices[] = array( - 'type' => 'confirmation', - // translators: placeholders are opening and closing strong tags. - 'text' => sprintf( esc_html__( '%1$sPayPal Reference Transactions are enabled on your account%2$s. All subscription management features are now enabled. Happy selling!', 'woocommerce-subscriptions' ), - '', - '' - ), - ); - } - - if ( false !== get_option( 'wcs_paypal_credentials_error' ) ) { - $notices[] = array( - 'type' => 'error', - // translators: placeholders are link opening and closing tags. 1$-2$: to gateway settings, 3$-4$: support docs on woocommerce.com - 'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your API credentials may be incorrect. Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s.', 'woocommerce-subscriptions' ), - '', - '', - '', - '' - ), - ); - } - - if ( 'yes' == get_option( 'wcs_paypal_invalid_profile_id' ) ) { - $notices[] = array( - 'type' => 'error', - // translators: placeholders are opening and closing link tags. 1$-2$: docs on woocommerce, 3$-4$: dismiss link - 'text' => sprintf( esc_html__( 'There is a problem with PayPal. Your PayPal account is issuing out-of-date subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s.', 'woocommerce-subscriptions' ), - '', - '', - '', - '' - ), - ); - } - - $last_ipn_error = get_option( 'wcs_fatal_error_handling_ipn', '' ); - $failed_ipn_log_handle = 'wcs-ipn-failures'; - - if ( ! empty( $last_ipn_error ) && ( false == get_option( 'wcs_fatal_error_handling_ipn_ignored', false ) || isset( $_GET['wcs_reveal_your_ipn_secrets'] ) ) ) { - $notices[] = array( - 'type' => 'error', - 'text' => sprintf( esc_html__( '%sA fatal error has occurred when processing a recent subscription payment with PayPal. Please %sopen a new ticket at WooCommerce Support%s immediately to get this resolved.%sIn order to get the quickest possible response please attach a %sTemporary Admin Login%s and a copy of your PHP error logs to your support ticket.%sLast recorded error: %sTo see the full error, view the %s log file from the %sWooCommerce logs screen.%s', 'woocommerce-subscriptions' ), - '

', - '', - '', - '
', - '', - '', - '

', - '' . esc_html( $last_ipn_error ) . '

', - '' . $failed_ipn_log_handle . '', - '', - '

' . esc_html__( 'Ignore this error (not recommended!)', 'woocommerce-subscriptions' ) . ' ' . esc_html__( 'Open up a ticket now!', 'woocommerce-subscriptions' ) . '
' - ), - ); - } + $notice->display(); } if ( ! empty( $notices ) ) { diff --git a/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php b/includes/gateways/paypal/includes/admin/class-wcs-paypal-change-payment-method-admin.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-billing-agreement.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-billing-agreement.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-checkout.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-checkout.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-payment.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-recurring-payment.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response-recurring-payment.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-response.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php old mode 100755 new mode 100644 index 63f0f49..1db6adf --- a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api.php @@ -22,8 +22,6 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -require_once( 'abstracts/abstract-wcs-sv-api-base.php' ); - class WCS_PayPal_Reference_Transaction_API extends WCS_SV_API_Base { /** the production endpoint */ diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-ipn-handler.php b/includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-ipn-handler.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php old mode 100755 new mode 100644 index 3d61926..b95d761 --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-change-payment-method.php @@ -91,6 +91,4 @@ class WCS_PayPal_Standard_Change_Payment_Method { return $change_button_text; } - } -WCS_PayPal_Standard_Change_Payment_Method::init(); diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-failure-handler.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-failure-handler.php old mode 100755 new mode 100644 index c446e3e..2d8a0b1 --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-failure-handler.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-failure-handler.php @@ -19,6 +19,9 @@ class WCS_PayPal_Standard_IPN_Failure_Handler { private static $transaction_details = null; + /** + * @var WC_Logger_Interface|null + */ public static $log = null; /** diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php old mode 100755 new mode 100644 index 96e9b3d..496e3ff --- a/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php +++ b/includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php @@ -187,10 +187,10 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { // If the invoice ID doesn't match the default invoice ID and contains the string '-wcsfrp-', the IPN is for a subscription payment to fix up a failed payment if ( in_array( $transaction_details['txn_type'], array( 'subscr_signup', 'subscr_payment' ) ) && false !== strpos( $transaction_details['invoice'], '-wcsfrp-' ) ) { - $renewal_order = wc_get_order( substr( $transaction_details['invoice'], strrpos( $transaction_details['invoice'], '-' ) + 1 ) ); + $transaction_order = wc_get_order( substr( $transaction_details['invoice'], strrpos( $transaction_details['invoice'], '-' ) + 1 ) ); // check if the failed signup has been previously recorded - if ( wcs_get_objects_property( $renewal_order, 'id' ) != get_post_meta( $subscription->get_id(), '_paypal_failed_sign_up_recorded', true ) ) { + if ( wcs_get_objects_property( $transaction_order, 'id' ) != get_post_meta( $subscription->get_id(), '_paypal_failed_sign_up_recorded', true ) ) { $is_renewal_sign_up_after_failure = true; } } @@ -302,15 +302,26 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { add_action( 'woocommerce_subscription_on-hold_paypal', 'WCS_PayPal_Status_Manager::suspend_subscription' ); } - // Generate a renewal order to record the payment (and determine how much is due) - $renewal_order = $this->get_renewal_order_by_transaction_id( $subscription, $transaction_details['txn_id'] ); - if ( is_null( $renewal_order ) ) { - $renewal_order = wcs_create_renewal_order( $subscription ); + // Gets renewals order based on transaction id. + $transaction_order = $this->get_renewal_order_by_transaction_id( $subscription, $transaction_details['txn_id'] ); + if ( is_null( $transaction_order ) ) { + // if renewal order is null, search for a parent order. + $transaction_order = $this->get_parent_order_by_transaction_id( $subscription, $transaction_details['txn_id'] ); + + // If this transaction id is linked to a parent order, we need to set $is_first_payment to true. + if ( ! is_null( $transaction_order ) ) { + $is_first_payment = true; + } } - // Set PayPal as the payment method (we can't use $renewal_order->set_payment_method() here as it requires an object we don't have) + // If we still have a non-valid order, let's create a renewal order. + if ( is_null( $transaction_order ) ) { + $transaction_order = wcs_create_renewal_order( $subscription ); + } + + // Set PayPal as the payment method. $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - $renewal_order->set_payment_method( $available_gateways['paypal'] ); + $transaction_order->set_payment_method( $available_gateways['paypal'] ); } if ( 'completed' == strtolower( $transaction_details['payment_status'] ) ) { @@ -324,8 +335,14 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { // First payment on order, process payment & activate subscription if ( $is_first_payment ) { + $parent_order = $subscription->get_parent(); - $subscription->get_parent()->payment_complete( $transaction_details['txn_id'] ); + if ( ! $parent_order->is_paid() ) { + $parent_order->payment_complete( $transaction_details['txn_id'] ); + } elseif ( $subscription->can_be_updated_to( 'active' ) ) { + // If the order has already been paid it might have been completed via PDT so reactivate the subscription now because calling payment complete won't. + $subscription->update_status( 'active' ); + } // Store PayPal Details on Order $this->save_paypal_meta_data( $subscription->get_parent(), $transaction_details ); @@ -343,9 +360,9 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { // Process the payment if the subscription is active } elseif ( ! $subscription->has_status( array( 'cancelled', 'expired', 'switched', 'trash' ) ) ) { - if ( true === $is_renewal_sign_up_after_failure && is_object( $renewal_order ) ) { + if ( true === $is_renewal_sign_up_after_failure && is_object( $transaction_order ) ) { - update_post_meta( $subscription->get_id(), '_paypal_failed_sign_up_recorded', wcs_get_objects_property( $renewal_order, 'id' ) ); + update_post_meta( $subscription->get_id(), '_paypal_failed_sign_up_recorded', wcs_get_objects_property( $transaction_order, 'id' ) ); // We need to cancel the old subscription now that the method has been changed successfully if ( 'paypal' == get_post_meta( $subscription->get_id(), '_old_payment_method', true ) ) { @@ -363,7 +380,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { try { - // to cover the case when PayPal drank too much coffee and sent IPNs early - needs to happen before $renewal_order->payment_complete + // to cover the case when PayPal drank too much coffee and sent IPNs early - needs to happen before $transaction_order->payment_complete $update_dates = array(); if ( $subscription->get_time( 'trial_end' ) > gmdate( 'U' ) ) { @@ -390,16 +407,16 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { remove_action( 'woocommerce_subscription_activated_paypal', 'WCS_PayPal_Status_Manager::reactivate_subscription' ); try { - $renewal_order->payment_complete( $transaction_details['txn_id'] ); + $transaction_order->payment_complete( $transaction_details['txn_id'] ); } catch ( Exception $e ) { - WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment exception calling $renewal_order->payment_complete() for subscription %d: %s.', $subscription->get_id(), $e->getMessage() ) ); + WC_Gateway_Paypal::log( sprintf( 'IPN subscription payment exception calling $transaction_order->payment_complete() for subscription %d: %s.', $subscription->get_id(), $e->getMessage() ) ); } - $this->add_order_note( __( 'IPN subscription payment completed.', 'woocommerce-subscriptions' ), $renewal_order, $transaction_details ); + $this->add_order_note( __( 'IPN subscription payment completed.', 'woocommerce-subscriptions' ), $transaction_order, $transaction_details ); add_action( 'woocommerce_subscription_activated_paypal', 'WCS_PayPal_Status_Manager::reactivate_subscription' ); - wcs_set_paypal_id( $renewal_order, $transaction_details['subscr_id'] ); + wcs_set_paypal_id( $transaction_order, $transaction_details['subscr_id'] ); } } elseif ( in_array( strtolower( $transaction_details['payment_status'] ), array( 'pending', 'failed' ) ) ) { @@ -409,16 +426,16 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { if ( ! $is_first_payment ) { - wcs_set_objects_property( $renewal_order, 'transaction_id', $transaction_details['txn_id'] ); + wcs_set_objects_property( $transaction_order, 'transaction_id', $transaction_details['txn_id'] ); if ( 'failed' == strtolower( $transaction_details['payment_status'] ) ) { $subscription->payment_failed(); // translators: placeholder is payment status (e.g. "completed") - $this->add_order_note( sprintf( _x( 'IPN subscription payment %s.', 'used in order note', 'woocommerce-subscriptions' ), $transaction_details['payment_status'] ), $renewal_order, $transaction_details ); + $this->add_order_note( sprintf( _x( 'IPN subscription payment %s.', 'used in order note', 'woocommerce-subscriptions' ), $transaction_details['payment_status'] ), $transaction_order, $transaction_details ); } else { - $renewal_order->update_status( 'on-hold' ); + $transaction_order->update_status( 'on-hold' ); // translators: placeholder is payment status (e.g. "completed") - $this->add_order_note( sprintf( _x( 'IPN subscription payment %s for reason: %s.', 'used in order note', 'woocommerce-subscriptions' ), $transaction_details['payment_status'], $transaction_details['pending_reason'] ), $renewal_order, $transaction_details ); + $this->add_order_note( sprintf( _x( 'IPN subscription payment %s for reason: %s.', 'used in order note', 'woocommerce-subscriptions' ), $transaction_details['payment_status'], $transaction_details['pending_reason'] ), $transaction_order, $transaction_details ); } } @@ -490,12 +507,12 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { if ( ! $is_first_payment && ! $is_renewal_sign_up_after_failure && $subscription->has_status( 'active' ) ) { // Generate a renewal order to record the failed payment - $renewal_order = wcs_create_renewal_order( $subscription ); + $transaction_order = wcs_create_renewal_order( $subscription ); // Set PayPal as the payment method $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); - $renewal_order->set_payment_method( $available_gateways['paypal'] ); - $this->add_order_note( $ipn_failure_note, $renewal_order, $transaction_details ); + $transaction_order->set_payment_method( $available_gateways['paypal'] ); + $this->add_order_note( $ipn_failure_note, $transaction_order, $transaction_details ); } WC_Gateway_Paypal::log( 'IPN subscription payment failure for subscription ' . $subscription->get_id() ); @@ -690,16 +707,17 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { } /** - * Get a renewal order associated with a subscription that has a specified transaction id. - * - * @param WC_Subscription object $subscription - * @param int $transaction_id Id from transaction details as provided by PayPal - * @return WC_Order|null If order with that transaction id, WC_Order object, otherwise null - * @since 2.1 - */ - protected function get_renewal_order_by_transaction_id( $subscription, $transaction_id ) { - - $orders = $subscription->get_related_orders( 'all', 'renewal' ); + * Get an order associated with a subscription that has a specified transaction id. + * + * @param WC_Subscription object $subscription + * @param int $transaction_id Id from transaction details as provided by PayPal + * @param array|string Order type we want. Defaults to any. + * + * @return WC_Order|null If order with that transaction id, WC_Order object, otherwise null + * @since 2.4.3 + */ + protected function get_order_by_transaction_id( $subscription, $transaction_id, $order_types = 'any' ) { + $orders = $subscription->get_related_orders( 'all', $order_types ); $renewal_order = null; foreach ( $orders as $order ) { @@ -711,4 +729,29 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler { return $renewal_order; } + + /** + * Get a renewal order associated with a subscription that has a specified transaction id. + * + * @param WC_Subscription object $subscription + * @param int $transaction_id Id from transaction details as provided by PayPal + * @return WC_Order|null If order with that transaction id, WC_Order object, otherwise null + * @since 2.1 + */ + protected function get_renewal_order_by_transaction_id( $subscription, $transaction_id ) { + return self::get_order_by_transaction_id( $subscription, $transaction_id, 'renewal' ); + } + + /** + * Get a parent order associated with a subscription that has a specified transaction id. + * + * @param WC_Subscription object $subscription + * @param int $transaction_id Id from transaction details as provided by PayPal + * + * @return WC_Order|null If order with that transaction id, WC_Order object, otherwise null + * @since 2.4.3 + */ + protected function get_parent_order_by_transaction_id( $subscription, $transaction_id ) { + return self::get_order_by_transaction_id( $subscription, $transaction_id, 'parent' ); + } } diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-request.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-request.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-standard-switcher.php b/includes/gateways/paypal/includes/class-wcs-paypal-standard-switcher.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-status-manager.php b/includes/gateways/paypal/includes/class-wcs-paypal-status-manager.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/class-wcs-paypal-supports.php b/includes/gateways/paypal/includes/class-wcs-paypal-supports.php old mode 100755 new mode 100644 diff --git a/includes/gateways/paypal/includes/deprecated/class-wc-paypal-standard-subscriptions.php b/includes/gateways/paypal/includes/deprecated/class-wc-paypal-standard-subscriptions.php old mode 100755 new mode 100644 index f9125d9..62b589e --- a/includes/gateways/paypal/includes/deprecated/class-wc-paypal-standard-subscriptions.php +++ b/includes/gateways/paypal/includes/deprecated/class-wc-paypal-standard-subscriptions.php @@ -269,7 +269,6 @@ class WC_PayPal_Standard_Subscriptions { return wcs_calculate_paypal_trial_periods_until( $future_timestamp ); } } -add_action( 'init', 'WC_PayPal_Standard_Subscriptions::init', 11 ); /** * Needs to be called after init so that $woocommerce global is setup diff --git a/includes/gateways/paypal/includes/templates/admin-notices.php b/includes/gateways/paypal/includes/templates/admin-notices.php old mode 100755 new mode 100644 index be20763..9a924fb --- a/includes/gateways/paypal/includes/templates/admin-notices.php +++ b/includes/gateways/paypal/includes/templates/admin-notices.php @@ -13,21 +13,25 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -foreach ( $notices as $notice ) : ?> - - 'error', + 'text' => '', + ) ); + + switch ( $notice_args['type'] ) { case 'warning' : - echo '
'; + $notice = new WCS_Admin_Notice( 'updated', array( 'style' => array( 'border-left: 4px solid #ffba00' ) ) ); break; case 'error' : - echo '
'; + $notice = new WCS_Admin_Notice( 'updated error' ); break; case 'confirmation' : default : - echo '
'; + $notice = new WCS_Admin_Notice( 'updated' ); break; - } ?> -

-
-set_simple_content( $notice_args['text'] ); + $notice->display(); +} diff --git a/includes/gateways/paypal/includes/templates/html-ipn-failure-notice.php b/includes/gateways/paypal/includes/templates/html-ipn-failure-notice.php new file mode 100644 index 0000000..659d566 --- /dev/null +++ b/includes/gateways/paypal/includes/templates/html-ipn-failure-notice.php @@ -0,0 +1,43 @@ + +

', + '', + '' + );?> +

+

', + '' + );?> +

+ + + +

' . esc_html( $failed_ipn_log_handle ) . '', + '', + '' + );?> +

+ + diff --git a/includes/gateways/paypal/includes/wcs-paypal-functions.php b/includes/gateways/paypal/includes/wcs-paypal-functions.php old mode 100755 new mode 100644 diff --git a/includes/interfaces/interface-wcs-cache-updater.php b/includes/interfaces/interface-wcs-cache-updater.php old mode 100755 new mode 100644 diff --git a/includes/legacy/class-wc-product-subscription-legacy.php b/includes/legacy/class-wc-product-subscription-legacy.php old mode 100755 new mode 100644 diff --git a/includes/legacy/class-wc-product-subscription-variation-legacy.php b/includes/legacy/class-wc-product-subscription-variation-legacy.php old mode 100755 new mode 100644 diff --git a/includes/legacy/class-wc-product-variable-subscription-legacy.php b/includes/legacy/class-wc-product-variable-subscription-legacy.php old mode 100755 new mode 100644 diff --git a/includes/legacy/class-wc-subscription-legacy.php b/includes/legacy/class-wc-subscription-legacy.php old mode 100755 new mode 100644 index 5392c61..9a4e4f7 --- a/includes/legacy/class-wc-subscription-legacy.php +++ b/includes/legacy/class-wc-subscription-legacy.php @@ -425,23 +425,26 @@ class WC_Subscription_Legacy extends WC_Subscription { $sign_up_fee = $original_order_item['line_total'] / $original_order_item['qty']; } elseif ( isset( $original_order_item['item_meta']['_synced_sign_up_fee'] ) ) { $sign_up_fee = $original_order_item['item_meta']['_synced_sign_up_fee'] / $original_order_item['qty']; + + // The synced sign up fee meta contains the raw product sign up fee, if the subscription totals are inclusive of tax, we need to adjust the synced sign up fee to match tax inclusivity. + if ( $this->get_prices_include_tax() ) { + $line_item_total = $original_order_item['line_total'] + $original_order_item['line_tax']; + $signup_fee_portion = $sign_up_fee / $line_item_total; + $sign_up_fee = $original_order_item['line_total'] * $signup_fee_portion; + } } else { // Sign-up fee is any amount on top of recurring amount $sign_up_fee = max( $original_order_item['line_total'] / $original_order_item['qty'] - $line_item['line_total'] / $line_item['qty'], 0 ); } - if ( ! empty( $original_order_item ) && ! empty( $sign_up_fee ) ) { + // If prices don't inc tax, ensure that the sign up fee amount includes the tax. + if ( 'inclusive_of_tax' === $tax_inclusive_or_exclusive && ! empty( $original_order_item ) && ! empty( $sign_up_fee ) ) { $sign_up_fee_proportion = $sign_up_fee / ( $original_order_item['line_total'] / $original_order_item['qty'] ); - $sign_up_fee_tax = wc_round_tax_total( $original_order_item['line_tax'] * $sign_up_fee_proportion ); + $sign_up_fee_tax = $original_order_item['line_tax'] * $sign_up_fee_proportion; - // If prices don't inc tax, ensure that the sign up fee amount includes the tax. - if ( 'inclusive_of_tax' === $tax_inclusive_or_exclusive && ! $this->get_prices_include_tax() ) { - $sign_up_fee += $sign_up_fee_tax; - // If prices inc tax and the request is for prices exclusive of tax, remove the taxes. - } elseif ( 'inclusive_of_tax' !== $tax_inclusive_or_exclusive && $this->get_prices_include_tax() ) { - $sign_up_fee -= $sign_up_fee_tax; - } + $sign_up_fee += $sign_up_fee_tax; + $sign_up_fee = wc_format_decimal( $sign_up_fee, wc_get_price_decimals() ); } } diff --git a/includes/legacy/class-wcs-array-property-post-meta-black-magic.php b/includes/legacy/class-wcs-array-property-post-meta-black-magic.php old mode 100755 new mode 100644 diff --git a/includes/legacy/class-wcs-product-legacy.php b/includes/legacy/class-wcs-product-legacy.php old mode 100755 new mode 100644 index 80ff38f..d762201 --- a/includes/legacy/class-wcs-product-legacy.php +++ b/includes/legacy/class-wcs-product-legacy.php @@ -39,4 +39,3 @@ class WCS_Product_Legacy { } } -WCS_Product_Legacy::init(); diff --git a/includes/libraries/action-scheduler/.github/release-drafter.yml b/includes/libraries/action-scheduler/.github/release-drafter.yml new file mode 100644 index 0000000..702a690 --- /dev/null +++ b/includes/libraries/action-scheduler/.github/release-drafter.yml @@ -0,0 +1,15 @@ +template: | + ## next release – date + + + + $CHANGES + + **Added** + **Changed** + **Deprecated** + **Removed** + **Fixed** + **Security** + +change-template: '* $TITLE (PR #$NUMBER)' diff --git a/includes/libraries/action-scheduler/action-scheduler.php b/includes/libraries/action-scheduler/action-scheduler.php old mode 100755 new mode 100644 index 96595cd..28b37f1 --- a/includes/libraries/action-scheduler/action-scheduler.php +++ b/includes/libraries/action-scheduler/action-scheduler.php @@ -5,7 +5,7 @@ * Description: A robust scheduling library for use in WordPress plugins. * Author: Prospress * Author URI: http://prospress.com/ - * Version: 2.0.0 + * Version: 2.1.1 * License: GPLv3 * * Copyright 2018 Prospress, Inc. (email : freedoms@prospress.com) @@ -25,21 +25,21 @@ * */ -if ( ! function_exists( 'action_scheduler_register_2_dot_0_dot_0' ) ) { +if ( ! function_exists( 'action_scheduler_register_2_dot_1_dot_1' ) ) { if ( ! class_exists( 'ActionScheduler_Versions' ) ) { require_once( 'classes/ActionScheduler_Versions.php' ); add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); } - add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_0_dot_0', 0, 0 ); + add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_1_dot_1', 0, 0 ); - function action_scheduler_register_2_dot_0_dot_0() { + function action_scheduler_register_2_dot_1_dot_1() { $versions = ActionScheduler_Versions::instance(); - $versions->register( '2.0.0', 'action_scheduler_initialize_2_dot_0_dot_0' ); + $versions->register( '2.1.1', 'action_scheduler_initialize_2_dot_1_dot_1' ); } - function action_scheduler_initialize_2_dot_0_dot_0() { + function action_scheduler_initialize_2_dot_1_dot_1() { require_once( 'classes/ActionScheduler.php' ); ActionScheduler::init( __FILE__ ); } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler.php b/includes/libraries/action-scheduler/classes/ActionScheduler.php old mode 100755 new mode 100644 index 5e1ad36..51af2b5 --- a/includes/libraries/action-scheduler/classes/ActionScheduler.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler.php @@ -59,7 +59,9 @@ abstract class ActionScheduler { public static function autoload( $class ) { $d = DIRECTORY_SEPARATOR; - if ( strpos( $class, 'ActionScheduler' ) === 0 ) { + if ( 'Deprecated' === substr( $class, -10 ) ) { + $dir = self::plugin_path('deprecated'.$d); + } elseif ( strpos( $class, 'ActionScheduler' ) === 0 ) { $dir = self::plugin_path('classes'.$d); } elseif ( strpos( $class, 'CronExpression' ) === 0 ) { $dir = self::plugin_path('lib'.$d.'cron-expression'.$d); @@ -67,8 +69,8 @@ abstract class ActionScheduler { return; } - if ( file_exists( $dir.$class.'.php' ) ) { - include( $dir.$class.'.php' ); + if ( file_exists( "{$dir}{$class}.php" ) ) { + include( "{$dir}{$class}.php" ); return; } } @@ -97,6 +99,10 @@ abstract class ActionScheduler { require_once( self::plugin_path('functions.php') ); + if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) { + require_once( self::plugin_path('deprecated/functions.php') ); + } + if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' ); } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_ListTable.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_ListTable.php old mode 100755 new mode 100644 index de47067..6af17b9 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_ListTable.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_ListTable.php @@ -275,7 +275,7 @@ abstract class ActionScheduler_Abstract_ListTable extends WP_List_Table { $valid_sortable_columns = array_values( $this->sort_by ); if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns ) ) { - $orderby = wc_clean( $_GET['orderby'] ); + $orderby = sanitize_text_field( $_GET['orderby'] ); } else { $orderby = $valid_sortable_columns[0]; } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php old mode 100755 new mode 100644 index 21f9ee0..5259797 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Abstract_QueueRunner.php @@ -3,7 +3,7 @@ /** * Abstract class with common Queue Cleaner functionality. */ -abstract class ActionScheduler_Abstract_QueueRunner { +abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abstract_QueueRunner_Deprecated { /** @var ActionScheduler_QueueCleaner */ protected $cleaner; @@ -14,6 +14,16 @@ abstract class ActionScheduler_Abstract_QueueRunner { /** @var ActionScheduler_Store */ protected $store; + /** + * The created time. + * + * Represents when the queue runner was constructed and used when calculating how long a PHP request has been running. + * For this reason it should be as close as possible to the PHP request start time. + * + * @var int + */ + private $created_time; + /** * ActionScheduler_Abstract_QueueRunner constructor. * @@ -22,6 +32,9 @@ abstract class ActionScheduler_Abstract_QueueRunner { * @param ActionScheduler_QueueCleaner $cleaner */ public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) { + + $this->created_time = microtime( true ); + $this->store = $store ? $store : ActionScheduler_Store::instance(); $this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store ); $this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store ); @@ -35,6 +48,12 @@ abstract class ActionScheduler_Abstract_QueueRunner { public function process_action( $action_id ) { try { do_action( 'action_scheduler_before_execute', $action_id ); + + if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { + do_action( 'action_scheduler_execution_ignored', $action_id ); + return; + } + $action = $this->store->fetch_action( $action_id ); $this->store->log_execution( $action_id ); $action->execute(); @@ -79,6 +98,114 @@ abstract class ActionScheduler_Abstract_QueueRunner { return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 5 ); } + /** + * Get the maximum number of seconds a batch can run for. + * + * @return int The number of seconds. + */ + protected function get_time_limit() { + + $time_limit = 30; + + // Apply deprecated filter from deprecated get_maximum_execution_time() method + if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) { + _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' ); + $time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit ); + } + + return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) ); + } + + /** + * Get the number of seconds the process has been running. + * + * @return int The number of seconds. + */ + protected function get_execution_time() { + $execution_time = microtime( true ) - $this->created_time; + + // Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time. + if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) { + $resource_usages = getrusage(); + + if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) { + $execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 ); + } + } + + return $execution_time; + } + + /** + * Check if the host's max execution time is (likely) to be exceeded if processing more actions. + * + * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action + * @return bool + */ + protected function time_likely_to_be_exceeded( $processed_actions ) { + + $execution_time = $this->get_execution_time(); + $max_execution_time = $this->get_time_limit(); + $time_per_action = $execution_time / $processed_actions; + $estimated_time = $execution_time + ( $time_per_action * 3 ); + $likely_to_be_exceeded = $estimated_time > $max_execution_time; + + return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time ); + } + + /** + * Get memory limit + * + * Based on WP_Background_Process::get_memory_limit() + * + * @return int + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + $memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce + } + + if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) { + // Unlimited, set to 32GB. + $memory_limit = '32G'; + } + + return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit ); + } + + /** + * Memory exceeded + * + * Ensures the batch process never exceeds 90% of the maximum WordPress memory. + * + * Based on WP_Background_Process::memory_exceeded() + * + * @return bool + */ + protected function memory_exceeded() { + + $memory_limit = $this->get_memory_limit() * 0.90; + $current_memory = memory_get_usage( true ); + $memory_exceeded = $current_memory >= $memory_limit; + + return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this ); + } + + /** + * See if the batch limits have been exceeded, which is when memory usage is almost at + * the maximum limit, or the time to process more actions will exceed the max time limit. + * + * Based on WC_Background_Process::batch_limits_exceeded() + * + * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action + * @return bool + */ + protected function batch_limits_exceeded( $processed_actions ) { + return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions ); + } + /** * Process actions in the queue. * diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Action.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Action.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_ActionClaim.php b/includes/libraries/action-scheduler/classes/ActionScheduler_ActionClaim.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_ActionFactory.php b/includes/libraries/action-scheduler/classes/ActionScheduler_ActionFactory.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php b/includes/libraries/action-scheduler/classes/ActionScheduler_AdminView.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_CanceledAction.php b/includes/libraries/action-scheduler/classes/ActionScheduler_CanceledAction.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Compatibility.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Compatibility.php new file mode 100644 index 0000000..c06e5a4 --- /dev/null +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Compatibility.php @@ -0,0 +1,99 @@ + $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) { + if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) { + return $filtered_limit; + } else { + return false; + } + } elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) { + if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) { + return $wp_max_limit; + } else { + return false; + } + } + return false; + } + + /** + * Attempts to raise the PHP timeout for time intensive processes. + * + * Only allows raising the existing limit and prevents lowering it. Wrapper for wc_set_time_limit(), when available. + * + * @param int The time limit in seconds. + */ + public static function raise_time_limit( $limit = 0 ) { + if ( $limit < ini_get( 'max_execution_time' ) ) { + return; + } + + if ( function_exists( 'wc_set_time_limit' ) ) { + wc_set_time_limit( $limit ); + } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { + @set_time_limit( $limit ); + } + } +} diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_CronSchedule.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_DateTime.php b/includes/libraries/action-scheduler/classes/ActionScheduler_DateTime.php old mode 100755 new mode 100644 index 13d8c27..5e8743c --- a/includes/libraries/action-scheduler/classes/ActionScheduler_DateTime.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_DateTime.php @@ -7,6 +7,16 @@ */ class ActionScheduler_DateTime extends DateTime { + /** + * UTC offset. + * + * Only used when a timezone is not set. When a timezone string is + * used, this will be set to 0. + * + * @var int + */ + protected $utcOffset = 0; + /** * Get the unix timestamp of the current object. * @@ -17,4 +27,50 @@ class ActionScheduler_DateTime extends DateTime { public function getTimestamp() { return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); } + + /** + * Set the UTC offset. + * + * This represents a fixed offset instead of a timezone setting. + * + * @param $offset + */ + public function setUtcOffset( $offset ) { + $this->utcOffset = intval( $offset ); + } + + /** + * Returns the timezone offset. + * + * @return int + * @link http://php.net/manual/en/datetime.getoffset.php + */ + public function getOffset() { + return $this->utcOffset ? $this->utcOffset : parent::getOffset(); + } + + /** + * Set the TimeZone associated with the DateTime + * + * @param DateTimeZone $timezone + * + * @return static + * @link http://php.net/manual/en/datetime.settimezone.php + */ + public function setTimezone( $timezone ) { + $this->utcOffset = 0; + parent::setTimezone( $timezone ); + + return $this; + } + + /** + * Get the timestamp with the WordPress timezone offset added or subtracted. + * + * @since 3.0.0 + * @return int + */ + public function getOffsetTimestamp() { + return $this->getTimestamp() + $this->getOffset(); + } } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Exception.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Exception.php new file mode 100644 index 0000000..9ec713f --- /dev/null +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Exception.php @@ -0,0 +1,11 @@ +claim = NULL; $this->untrack_action(); remove_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); - remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0, 1 ); - remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0, 0 ); - remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0, 0 ); + remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0 ); + remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0 ); + remove_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0 ); + remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0 ); } public function track_current_action( $action_id ) { @@ -51,4 +53,3 @@ class ActionScheduler_FatalErrorMonitor { } } } - \ No newline at end of file diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_FinishedAction.php b/includes/libraries/action-scheduler/classes/ActionScheduler_FinishedAction.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_IntervalSchedule.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_InvalidActionException.php b/includes/libraries/action-scheduler/classes/ActionScheduler_InvalidActionException.php new file mode 100644 index 0000000..a7decdc --- /dev/null +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_InvalidActionException.php @@ -0,0 +1,28 @@ +'; foreach ( $row['args'] as $key => $value ) { - $row_html .= sprintf( '
  • %s => %s
  • ', esc_html( $key ), esc_html( $value ) ); + $row_html .= sprintf( '
  • %s => %s
  • ', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) ); } $row_html .= ''; diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_LogEntry.php b/includes/libraries/action-scheduler/classes/ActionScheduler_LogEntry.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php old mode 100755 new mode 100644 index 601e567..30933ce --- a/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Logger.php @@ -41,7 +41,58 @@ abstract class ActionScheduler_Logger { */ abstract public function get_logs( $action_id ); - abstract public function init(); + /** + * @codeCoverageIgnore + */ + public function init() { + add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ), 10, 1 ); + add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 ); + add_action( 'action_scheduler_before_execute', array( $this, 'log_started_action' ), 10, 1 ); + add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 1 ); + add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 2 ); + add_action( 'action_scheduler_failed_action', array( $this, 'log_timed_out_action' ), 10, 2 ); + add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 ); + add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 ); + add_action( 'action_scheduler_execution_ignored', array( $this, 'log_ignored_action' ), 10, 1 ); + } + + public function log_stored_action( $action_id ) { + $this->log( $action_id, __( 'action created', 'action-scheduler' ) ); + } + + public function log_canceled_action( $action_id ) { + $this->log( $action_id, __( 'action canceled', 'action-scheduler' ) ); + } + + public function log_started_action( $action_id ) { + $this->log( $action_id, __( 'action started', 'action-scheduler' ) ); + } + + public function log_completed_action( $action_id ) { + $this->log( $action_id, __( 'action complete', 'action-scheduler' ) ); + } + + public function log_failed_action( $action_id, \Exception $exception ) { + $this->log( $action_id, sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() ) ); + } + + public function log_timed_out_action( $action_id, $timeout ) { + $this->log( $action_id, sprintf( __( 'action timed out after %s seconds', 'action-scheduler' ), $timeout ) ); + } + + public function log_unexpected_shutdown( $action_id, $error ) { + if ( ! empty( $error ) ) { + $this->log( $action_id, sprintf( __( 'unexpected shutdown: PHP Fatal error %s in %s on line %s', 'action-scheduler' ), $error['message'], $error['file'], $error['line'] ) ); + } + } + + public function log_reset_action( $action_id ) { + $this->log( $action_id, __( 'action reset', 'action_scheduler' ) ); + } + + public function log_ignored_action( $action_id ) { + $this->log( $action_id, __( 'action ignored', 'action-scheduler' ) ); + } } \ No newline at end of file diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_NullAction.php b/includes/libraries/action-scheduler/classes/ActionScheduler_NullAction.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_NullLogEntry.php b/includes/libraries/action-scheduler/classes/ActionScheduler_NullLogEntry.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_NullSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_NullSchedule.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_QueueCleaner.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_QueueRunner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_QueueRunner.php old mode 100755 new mode 100644 index 387bb51..f1e5b83 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_QueueRunner.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_QueueRunner.php @@ -11,16 +11,6 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { /** @var ActionScheduler_QueueRunner */ private static $runner = null; - /** - * The created time. - * - * Represents when the queue runner was constructed and used when calculating how long a PHP request has been running. - * For this reason it should be as close as possible to the PHP request start time. - * - * @var int - */ - private $created_time; - /** * @return ActionScheduler_QueueRunner * @codeCoverageIgnore @@ -42,7 +32,6 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { */ public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) { parent::__construct( $store, $monitor, $cleaner ); - $this->created_time = microtime( true ); } /** @@ -61,43 +50,39 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { } public function run() { - @ini_set( 'memory_limit', apply_filters( 'admin_memory_limit', WP_MAX_MEMORY_LIMIT ) ); - @set_time_limit( apply_filters( 'action_scheduler_queue_runner_time_limit', 600 ) ); + ActionScheduler_Compatibility::raise_memory_limit(); + ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); do_action( 'action_scheduler_before_process_queue' ); $this->run_cleanup(); - $count = 0; + $processed_actions = 0; if ( $this->store->get_claim_count() < $this->get_allowed_concurrent_batches() ) { $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 ); - $count = $this->do_batch( $batch_size ); + do { + $processed_actions_in_batch = $this->do_batch( $batch_size ); + $processed_actions += $processed_actions_in_batch; + } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $processed_actions ) ); // keep going until we run out of actions, time, or memory } do_action( 'action_scheduler_after_process_queue' ); - return $count; + return $processed_actions; } protected function do_batch( $size = 100 ) { $claim = $this->store->stake_claim($size); $this->monitor->attach($claim); - $processed_actions = 0; - $maximum_execution_time = $this->get_maximum_execution_time(); + $processed_actions = 0; foreach ( $claim->get_actions() as $action_id ) { - if ( 0 !== $processed_actions ) { - $time_elapsed = $this->get_execution_time(); - $average_processing_time = $time_elapsed / $processed_actions; - - // Bail early if the time it has taken to process this batch is approaching the maximum execution time. - if ( $time_elapsed + ( $average_processing_time * 2 ) > $maximum_execution_time ) { - break; - } - } - // bail if we lost the claim if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ) ) ) { break; } $this->process_action( $action_id ); $processed_actions++; + + if ( $this->batch_limits_exceeded( $processed_actions ) ) { + break; + } } $this->store->release_claim($claim); $this->monitor->detach(); @@ -127,43 +112,4 @@ class ActionScheduler_QueueRunner extends ActionScheduler_Abstract_QueueRunner { return $schedules; } - - /** - * Get the maximum number of seconds a batch can run for. - * - * @return int The number of seconds. - */ - protected function get_maximum_execution_time() { - - // There are known hosts with a strict 60 second execution time. - if ( defined( 'WPENGINE_ACCOUNT' ) || defined( 'PANTHEON_ENVIRONMENT' ) ) { - $maximum_execution_time = 60; - } elseif ( false !== strpos( getenv( 'HOSTNAME' ), '.siteground.' ) ) { - $maximum_execution_time = 120; - } else { - $maximum_execution_time = ini_get( 'max_execution_time' ); - } - - return absint( apply_filters( 'action_scheduler_maximum_execution_time', $maximum_execution_time ) ); - } - - /** - * Get the number of seconds a batch has run for. - * - * @return int The number of seconds. - */ - protected function get_execution_time() { - $execution_time = microtime( true ) - $this->created_time; - - // Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time. - if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) { - $resource_usages = getrusage(); - - if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) { - $execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 ); - } - } - - return $execution_time; - } } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Schedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Schedule.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_SimpleSchedule.php b/includes/libraries/action-scheduler/classes/ActionScheduler_SimpleSchedule.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Store.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Store.php old mode 100755 new mode 100644 index d967b59..59ac236 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_Store.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_Store.php @@ -168,20 +168,11 @@ abstract class ActionScheduler_Store { if ( ! $next ) { throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'action-scheduler' ) ); } - $next->setTimezone( $this->get_local_timezone() ); + ActionScheduler_TimezoneHelper::set_local_timezone( $next ); return $next->format( 'Y-m-d H:i:s' ); } - /** - * Get the site's local time. Wrapper for ActionScheduler_TimezoneHelper::get_local_timezone(). - * - * @return DateTimeZone - */ - protected function get_local_timezone() { - return ActionScheduler_TimezoneHelper::get_local_timezone(); - } - /** * @return array */ @@ -207,4 +198,15 @@ abstract class ActionScheduler_Store { } return self::$store; } + + /** + * Get the site's local time. + * + * @deprecated 2.1.0 + * @return DateTimeZone + */ + protected function get_local_timezone() { + _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); + return ActionScheduler_TimezoneHelper::get_local_timezone(); + } } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_TimezoneHelper.php b/includes/libraries/action-scheduler/classes/ActionScheduler_TimezoneHelper.php old mode 100755 new mode 100644 index 0c8d6e4..fd01449 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_TimezoneHelper.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_TimezoneHelper.php @@ -5,7 +5,102 @@ */ abstract class ActionScheduler_TimezoneHelper { private static $local_timezone = NULL; + + /** + * Set a DateTime's timezone to the WordPress site's timezone, or a UTC offset + * if no timezone string is available. + * + * @since 2.1.0 + * + * @param DateTime $date + * @return ActionScheduler_DateTime + */ + public static function set_local_timezone( DateTime $date ) { + + // Accept a DateTime for easier backward compatibility, even though we require methods on ActionScheduler_DateTime + if ( ! is_a( $date, 'ActionScheduler_DateTime' ) ) { + $date = as_get_datetime_object( $date->format( 'U' ) ); + } + + if ( get_option( 'timezone_string' ) ) { + $date->setTimezone( new DateTimeZone( self::get_local_timezone_string() ) ); + } else { + $date->setUtcOffset( self::get_local_timezone_offset() ); + } + + return $date; + } + + /** + * Helper to retrieve the timezone string for a site until a WP core method exists + * (see https://core.trac.wordpress.org/ticket/24730). + * + * Adapted from wc_timezone_string() and https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. + * + * If no timezone string is set, and its not possible to match the UTC offset set for the site to a timezone + * string, then an empty string will be returned, and the UTC offset should be used to set a DateTime's + * timezone. + * + * @since 2.1.0 + * @return string PHP timezone string for the site or empty if no timezone string is available. + */ + protected static function get_local_timezone_string( $reset = false ) { + // If site timezone string exists, return it. + $timezone = get_option( 'timezone_string' ); + if ( $timezone ) { + return $timezone; + } + + // Get UTC offset, if it isn't set then return UTC. + $utc_offset = intval( get_option( 'gmt_offset', 0 ) ); + if ( 0 === $utc_offset ) { + return 'UTC'; + } + + // Adjust UTC offset from hours to seconds. + $utc_offset *= 3600; + + // Attempt to guess the timezone string from the UTC offset. + $timezone = timezone_name_from_abbr( '', $utc_offset ); + if ( $timezone ) { + return $timezone; + } + + // Last try, guess timezone string manually. + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { + return $city['timezone_id']; + } + } + } + + // No timezone string + return ''; + } + + /** + * Get timezone offset in seconds. + * + * @since 2.1.0 + * @return float + */ + protected static function get_local_timezone_offset() { + $timezone = get_option( 'timezone_string' ); + + if ( $timezone ) { + $timezone_object = new DateTimeZone( $timezone ); + return $timezone_object->getOffset( new DateTime( 'now' ) ); + } else { + return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; + } + } + + /** + * @deprecated 2.1.0 + */ public static function get_local_timezone( $reset = FALSE ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); if ( $reset ) { self::$local_timezone = NULL; } @@ -18,18 +113,32 @@ abstract class ActionScheduler_TimezoneHelper { $tzstring = 'UTC'; } else { $gmt_offset *= HOUR_IN_SECONDS; - $tzstring = timezone_name_from_abbr('', $gmt_offset); + $tzstring = timezone_name_from_abbr( '', $gmt_offset, 1 ); + + // If there's no timezone string, try again with no DST. + if ( false === $tzstring ) { + $tzstring = timezone_name_from_abbr( '', $gmt_offset, 0 ); + } + + // Try mapping to the first abbreviation we can find. if ( false === $tzstring ) { $is_dst = date( 'I' ); foreach ( timezone_abbreviations_list() as $abbr ) { foreach ( $abbr as $city ) { if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) { + // If there's no valid timezone ID, keep looking. + if ( null === $city['timezone_id'] ) { + continue; + } + $tzstring = $city['timezone_id']; break 2; } } } } + + // If we still have no valid string, then fall back to UTC. if ( false === $tzstring ) { $tzstring = 'UTC'; } @@ -41,4 +150,3 @@ abstract class ActionScheduler_TimezoneHelper { return self::$local_timezone; } } - \ No newline at end of file diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_Versions.php b/includes/libraries/action-scheduler/classes/ActionScheduler_Versions.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php b/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_QueueRunner.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_Scheduler_command.php b/includes/libraries/action-scheduler/classes/ActionScheduler_WPCLI_Scheduler_command.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php old mode 100755 new mode 100644 index 78a9c29..7215ddd --- a/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_wpCommentLogger.php @@ -18,15 +18,16 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger { if ( empty($date) ) { $date = as_get_datetime_object(); } else { - $date = clone $date; + $date = as_get_datetime_object( clone $date ); } $comment_id = $this->create_wp_comment( $action_id, $message, $date ); return $comment_id; } protected function create_wp_comment( $action_id, $message, DateTime $date ) { + $comment_date_gmt = $date->format('Y-m-d H:i:s'); - $date->setTimezone( ActionScheduler_TimezoneHelper::get_local_timezone() ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); $comment_data = array( 'comment_post_ID' => $action_id, 'comment_date' => $date->format('Y-m-d H:i:s'), @@ -51,7 +52,7 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger { } $date = as_get_datetime_object( $comment->comment_date_gmt ); - $date->setTimezone( ActionScheduler_TimezoneHelper::get_local_timezone() ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date ); } @@ -217,14 +218,9 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger { public function init() { add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 ); add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 ); - add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ), 10, 1 ); - add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 ); - add_action( 'action_scheduler_before_execute', array( $this, 'log_started_action' ), 10, 1 ); - add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 1 ); - add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 2 ); - add_action( 'action_scheduler_failed_action', array( $this, 'log_timed_out_action' ), 10, 2 ); - add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 ); - add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 ); + + parent::init(); + add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 ); add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 ); @@ -241,38 +237,4 @@ class ActionScheduler_wpCommentLogger extends ActionScheduler_Logger { wp_defer_comment_counting(false); } - public function log_stored_action( $action_id ) { - $this->log( $action_id, __('action created', 'action-scheduler') ); - } - - public function log_canceled_action( $action_id ) { - $this->log( $action_id, __('action canceled', 'action-scheduler') ); - } - - public function log_started_action( $action_id ) { - $this->log( $action_id, __('action started', 'action-scheduler') ); - } - - public function log_completed_action( $action_id ) { - $this->log( $action_id, __('action complete', 'action-scheduler') ); - } - - public function log_failed_action( $action_id, Exception $exception ) { - $this->log( $action_id, sprintf(__('action failed: %s', 'action-scheduler'), $exception->getMessage() )); - } - - public function log_timed_out_action( $action_id, $timeout) { - $this->log( $action_id, sprintf( __('action timed out after %s seconds', 'action-scheduler'), $timeout ) ); - } - - public function log_unexpected_shutdown( $action_id, $error ) { - if ( !empty($error) ) { - $this->log( $action_id, sprintf(__('unexpected shutdown: PHP Fatal error %s in %s on line %s', 'action-scheduler'), $error['message'], $error['file'], $error['line'] ) ); - } - } - - public function log_reset_action( $action_id ) { - $this->log( $action_id, __('action reset', 'action_scheduler') ); - } - } diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore.php old mode 100755 new mode 100644 index 030e65d..efddb68 --- a/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore.php +++ b/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore.php @@ -11,8 +11,12 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { /** @var DateTimeZone */ protected $local_timezone = NULL; + /** @var int */ + private static $max_index_length = 191; + public function save_action( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ){ try { + $this->validate_action( $action ); $post_array = $this->create_post_array( $action, $scheduled_date ); $post_id = $this->save_post_array( $post_array ); $this->save_post_schedule( $post_id, $action->get_schedule() ); @@ -91,6 +95,12 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { protected function make_action_from_post( $post ) { $hook = $post->post_title; $args = json_decode( $post->post_content, true ); + + // Handle args that do not decode properly. + if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $args ) ) { + throw ActionScheduler_InvalidActionException::from_decoding_args( $post->ID ); + } + $schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true ); if ( empty($schedule) ) { $schedule = new ActionScheduler_NullSchedule(); @@ -410,18 +420,18 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { * @param string $action_id * * @throws InvalidArgumentException - * @return DateTime The date the action is schedule to run, or the date that it ran. + * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. */ public function get_date( $action_id ) { - $date = $this->get_date_gmt( $action_id ); - return $date->setTimezone( $this->get_local_timezone() ); + $next = $this->get_date_gmt( $action_id ); + return ActionScheduler_TimezoneHelper::set_local_timezone( $next ); } /** * @param string $action_id * * @throws InvalidArgumentException - * @return DateTime The date the action is schedule to run, or the date that it ran. + * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. */ public function get_date_gmt( $action_id ) { $post = get_post($action_id); @@ -716,6 +726,21 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store { } } + /** + * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4. + * + * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However, + * as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn + * developers of this impending requirement. + * + * @param ActionScheduler_Action $action + */ + protected function validate_action( ActionScheduler_Action $action ) { + if ( strlen( json_encode( $action->get_args() ) ) > self::$max_index_length ) { + _doing_it_wrong( 'ActionScheduler_Action::$args', sprintf( 'To ensure the action args column can be indexed, action args should not be more than %d characters when encoded as JSON. Support for strings longer than this will be removed in a future version.', self::$max_index_length ), '2.1.0' ); + } + } + /** * @codeCoverageIgnore */ diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore_PostStatusRegistrar.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore_PostStatusRegistrar.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore_PostTypeRegistrar.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore_PostTypeRegistrar.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore_TaxonomyRegistrar.php b/includes/libraries/action-scheduler/classes/ActionScheduler_wpPostStore_TaxonomyRegistrar.php old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/composer.json b/includes/libraries/action-scheduler/composer.json old mode 100755 new mode 100644 index fa07bc6..4df2c2d --- a/includes/libraries/action-scheduler/composer.json +++ b/includes/libraries/action-scheduler/composer.json @@ -1,6 +1,5 @@ { "name": "prospress/action-scheduler", - "version": "2.0.0", "description": "Action Scheduler for WordPress and WooCommerce", "type": "wordpress-plugin", "license": "GPL-3.0", diff --git a/includes/libraries/action-scheduler/composer.lock b/includes/libraries/action-scheduler/composer.lock old mode 100755 new mode 100644 diff --git a/includes/libraries/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php b/includes/libraries/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php new file mode 100644 index 0000000..dac17aa --- /dev/null +++ b/includes/libraries/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php @@ -0,0 +1,27 @@ + '' - the name of the action that will be triggered + * 'args' => NULL - the args array that will be passed with the action + * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'group' => '' - the group the action belongs to + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * 'per_page' => 5 - Number of results to return + * 'offset' => 0 + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' + * 'order' => 'ASC' + * @param string $return_format OBJECT, ARRAY_A, or ids + * + * @deprecated 2.1.0 + * + * @return array + */ +function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_get_scheduled_actions()' ); + return as_get_scheduled_actions( $args, $return_format ); +} diff --git a/includes/libraries/action-scheduler/functions.php b/includes/libraries/action-scheduler/functions.php old mode 100755 new mode 100644 index 567ba68..2f27d7e --- a/includes/libraries/action-scheduler/functions.php +++ b/includes/libraries/action-scheduler/functions.php @@ -14,7 +14,7 @@ * * @return string The job ID */ -function wc_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { +function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { return ActionScheduler::factory()->single( $hook, $args, $timestamp, $group ); } @@ -29,7 +29,7 @@ function wc_schedule_single_action( $timestamp, $hook, $args = array(), $group = * * @return string The job ID */ -function wc_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { +function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { return ActionScheduler::factory()->recurring( $hook, $args, $timestamp, $interval_in_seconds, $group ); } @@ -54,18 +54,27 @@ function wc_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, * * @return string The job ID */ -function wc_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { +function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { return ActionScheduler::factory()->cron( $hook, $args, $timestamp, $schedule, $group ); } /** - * Cancel the next occurrence of a job. + * Cancel the next occurrence of a scheduled action. + * + * While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent + * all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in + * a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled + * only after the former action is run. If the next instance is never run, because it's unscheduled by this function, + * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled + * by this method also. * * @param string $hook The hook that the job will trigger * @param array $args Args that would have been passed to the job * @param string $group + * + * @return string The scheduled action ID if a scheduled action was found, or empty string if no matching action found. */ -function wc_unschedule_action( $hook, $args = array(), $group = '' ) { +function as_unschedule_action( $hook, $args = array(), $group = '' ) { $params = array(); if ( is_array($args) ) { $params['args'] = $args; @@ -74,11 +83,25 @@ function wc_unschedule_action( $hook, $args = array(), $group = '' ) { $params['group'] = $group; } $job_id = ActionScheduler::store()->find_action( $hook, $params ); - if ( empty($job_id) ) { - return; + + if ( ! empty( $job_id ) ) { + ActionScheduler::store()->cancel_action( $job_id ); } - ActionScheduler::store()->cancel_action( $job_id ); + return $job_id; +} + +/** + * Cancel all occurrences of a scheduled action. + * + * @param string $hook The hook that the job will trigger + * @param array $args Args that would have been passed to the job + * @param string $group + */ +function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) { + do { + $unscheduled_action = as_unschedule_action( $hook, $args, $group ); + } while ( ! empty( $unscheduled_action ) ); } /** @@ -88,7 +111,7 @@ function wc_unschedule_action( $hook, $args = array(), $group = '' ) { * * @return int|bool The timestamp for the next occurrence, or false if nothing was found */ -function wc_next_scheduled_action( $hook, $args = NULL, $group = '' ) { +function as_next_scheduled_action( $hook, $args = NULL, $group = '' ) { $params = array(); if ( is_array($args) ) { $params['args'] = $args; @@ -130,7 +153,7 @@ function wc_next_scheduled_action( $hook, $args = NULL, $group = '' ) { * * @return array */ -function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { +function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { $store = ActionScheduler::store(); foreach ( array('date', 'modified') as $key ) { if ( isset($args[$key]) ) { diff --git a/includes/libraries/action-scheduler/license.txt b/includes/libraries/action-scheduler/license.txt old mode 100755 new mode 100644 diff --git a/includes/libraries/class-wc-datetime.php b/includes/libraries/class-wc-datetime.php old mode 100755 new mode 100644 diff --git a/includes/payment-retry/class-wcs-retry-admin.php b/includes/payment-retry/class-wcs-retry-admin.php old mode 100755 new mode 100644 index 4a7303f..5f928df --- a/includes/payment-retry/class-wcs-retry-admin.php +++ b/includes/payment-retry/class-wcs-retry-admin.php @@ -37,8 +37,6 @@ class WCS_Retry_Admin { /** * Add a meta box to the Edit Order screen to display the retries relating to that order - * - * @return null */ public function add_meta_boxes() { global $current_screen, $post_ID; @@ -54,9 +52,10 @@ class WCS_Retry_Admin { * and when that is the case, do not display the next payment date (because it will still be set to the original * payment date, in the past). * - * @param bool $show_date_type - * @param string $date_key + * @param bool $show_date_type + * @param string $date_key * @param WC_Subscription $the_subscription + * * @return bool */ public function maybe_hide_date_type( $show_date_type, $date_key, $the_subscription ) { @@ -73,8 +72,9 @@ class WCS_Retry_Admin { /** * Dispay the number of retries on a renewal order in the Orders list table. * - * @param string $column The string of the current column - * @param int $post_id The ID of the order + * @param string $column The string of the current column + * @param int $post_id The ID of the order + * * @since 2.1 */ public static function add_column_content( $column, $post_id ) { @@ -95,19 +95,19 @@ class WCS_Retry_Admin { foreach ( $retry_counts as $retry_status => $retry_count ) { switch ( $retry_status ) { - case 'pending' : + case 'pending': $tool_tip .= sprintf( _n( '%d Pending Payment Retry', '%d Pending Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count ); break; - case 'processing' : + case 'processing': $tool_tip .= sprintf( _n( '%d Processing Payment Retry', '%d Processing Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count ); break; - case 'failed' : + case 'failed': $tool_tip .= sprintf( _n( '%d Failed Payment Retry', '%d Failed Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count ); break; - case 'complete' : + case 'complete': $tool_tip .= sprintf( _n( '%d Successful Payment Retry', '%d Successful Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count ); break; - case 'cancelled' : + case 'cancelled': $tool_tip .= sprintf( _n( '%d Cancelled Payment Retry', '%d Cancelled Payment Retries', $retry_count, 'woocommerce-subscriptions' ), $retry_count ); break; } @@ -124,11 +124,15 @@ class WCS_Retry_Admin { * Add a setting to enable/disable the retry system * * @param array + * * @return null */ public function add_settings( $settings ) { - $misc_section_end = wp_list_filter( $settings, array( 'id' => 'woocommerce_subscriptions_miscellaneous', 'type' => 'sectionend' ) ); + $misc_section_end = wp_list_filter( $settings, array( + 'id' => 'woocommerce_subscriptions_miscellaneous', + 'type' => 'sectionend', + ) ); $spliced_array = array_splice( $settings, key( $misc_section_end ), 0, array( array( @@ -148,6 +152,7 @@ class WCS_Retry_Admin { * Add system status information about custom retry rules. * * @param array $data + * * @return array */ public static function add_system_status_content( $data ) { @@ -155,6 +160,7 @@ class WCS_Retry_Admin { $has_custom_retry_rule_class = has_action( 'wcs_retry_rule_class' ); $has_custom_raw_retry_rule = has_action( 'wcs_get_retry_rule_raw' ); $has_custom_retry_rule = has_action( 'wcs_get_retry_rule' ); + $has_retry_on_post_store = WCS_Retry_Migrator::needs_migration(); $data['wcs_retry_rules_overridden'] = array( 'name' => _x( 'Custom Retry Rules', 'label for the system status page', 'woocommerce-subscriptions' ), @@ -188,6 +194,14 @@ class WCS_Retry_Admin { 'success' => ! $has_custom_retry_rule, ); + $data['wcs_retry_data_migration_status'] = array( + 'name' => _x( 'Retries Migration Status', 'label for the system status page', 'woocommerce-subscriptions' ), + 'label' => 'Retries Migration Status', + 'mark_icon' => $has_retry_on_post_store ? '' : 'yes', + 'note' => $has_retry_on_post_store ? 'In-Progress' : 'Completed', + 'mark' => ( $has_retry_on_post_store ) ? '' : 'yes', + ); + return $data; } } diff --git a/includes/payment-retry/class-wcs-retry-background-migrator.php b/includes/payment-retry/class-wcs-retry-background-migrator.php new file mode 100644 index 0000000..cd313fb --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-background-migrator.php @@ -0,0 +1,116 @@ +scheduled_hook = 'wcs_retries_migration_hook'; + $this->time_limit = 30; + + $this->destination_store = WCS_Retry_Stores::get_database_store(); + $this->source_store = WCS_Retry_Stores::get_post_store(); + + $migrator_class = apply_filters( 'wcs_retry_retry_migrator_class', 'WCS_Retry_Migrator' ); + $this->migrator = new $migrator_class( $this->source_store, $this->destination_store, new WC_Logger() ); + + $this->log_handle = 'wcs-retries-background-migrator'; + $this->logger = $logger; + } + + /** + * Get the items to be updated, if any. + * + * @return array An array of items to update, or empty array if there are no items to update. + * @since 2.4.0 + */ + protected function get_items_to_update() { + return $this->source_store->get_retries( array( 'limit' => 10 ) ); + } + + /** + * Run the update for a single item. + * + * @param WCS_Retry $retry The item to update. + * + * @return int|null + * @since 2.4.0 + */ + protected function update_item( $retry ) { + try { + if ( ! is_a( $retry, 'WCS_Retry' ) ) { + throw new Exception( 'The $retry parameter must be a valid WCS_Retry instance.' ); + } + + $new_item_id = $this->migrator->migrate_entry( $retry->get_id() ); + + $this->log( sprintf( 'Payment retry ID: %d, has been migrated to custom table with new ID: %d.', $retry->get_id(), $new_item_id ) ); + + return $new_item_id; + } catch ( Exception $e ) { + if ( is_object( $retry ) ) { + $retry_description = get_class( $retry ) . '(id=' . wcs_get_objects_property( $retry, 'id' ) . ')'; + } else { + $retry_description = wp_json_encode( $retry ); + } + + $this->log( sprintf( '--- Exception caught migrating Payment retry %s - exception message: %s ---', $retry_description, $e->getMessage() ) ); + + return null; + } + } + + /** + * Unscheduled the instance's hook in Action Scheduler + * @since 2.4.1 + */ + protected function unschedule_background_updates() { + parent::unschedule_background_updates(); + + $this->migrator->set_needs_migration(); + } +} diff --git a/includes/payment-retry/class-wcs-retry-database-store.php b/includes/payment-retry/class-wcs-retry-database-store.php new file mode 100644 index 0000000..91899dc --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-database-store.php @@ -0,0 +1,234 @@ + $retry->get_order_id(), + 'status' => $retry->get_status(), + 'date_gmt' => $retry->get_date_gmt(), + 'rule_raw' => wp_json_encode( $retry->get_rule()->get_raw_data() ), + ); + $query_format = array( + '%d', + '%s', + '%s', + '%s', + ); + + if ( $retry->get_id() > 0 ) { + $query_data['retry_id'] = $retry->get_id(); + $query_format[] = '%d'; + } + + if ( $retry->get_id() && $this->get_retry( $retry->get_id() ) ) { + $wpdb->update( + $this->get_full_table_name(), + $query_data, + array( 'retry_id' => $retry->get_id() ), + $query_format + ); + + $retry_id = absint( $retry->get_id() ); + } else { + $wpdb->insert( + $this->get_full_table_name(), + $query_data, + $query_format + ); + + $retry_id = absint( $wpdb->insert_id ); + } + + return $retry_id; + } + + /** + * Get the details of a retry from the database + * + * @param int $retry_id The retry we want to get. + * + * @return null|WCS_Retry + * @since 2.4 + */ + public function get_retry( $retry_id ) { + global $wpdb; + + $retry = null; + $raw_retry = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$this->get_full_table_name()} WHERE retry_id = %d LIMIT 1", + $retry_id + ) + ); + + if ( $raw_retry ) { + $retry = new WCS_Retry( array( + 'id' => $raw_retry->retry_id, + 'order_id' => $raw_retry->order_id, + 'status' => $raw_retry->status, + 'date_gmt' => $raw_retry->date_gmt, + 'rule_raw' => json_decode( $raw_retry->rule_raw ), + ) ); + } + + return $retry; + } + + /** + * Deletes a retry. + * + * @param int $retry_id + * + * @return bool + * @since 2.4 + */ + public function delete_retry( $retry_id ) { + global $wpdb; + + return (bool) $wpdb->delete( $this->get_full_table_name(), array( 'retry_id' => $retry_id ), array( '%d' ) ); + } + + /** + * Get a set of retries from the database + * + * @param array $args A set of filters: + * 'status': filter to only retries of a certain status, either 'pending', 'processing', 'failed' or 'complete'. Default: 'any', which will return all retries. + * 'date_query': array of dates to filter retries to those that occur 'after' or 'before' a certain date (or between those two dates). Should be a MySQL formated date/time string. + * 'orderby': Order by which property? + * 'order': Order in ASC/DESC. + * 'order_id': filter retries to those which belong to a certain order ID. + * 'limit': How many retries we want to get. + * @param string $return Defines in which format return the entries. options: + * 'objects': Returns an array of WCS_Retry objects + * 'ids': Returns an array of ids. + * + * @return array An array of WCS_Retry objects or ids. + * @since 2.4 + */ + public function get_retries( $args = array(), $return = 'objects' ) { + global $wpdb; + + $args = wp_parse_args( $args, array( + 'status' => 'any', + 'date_query' => array(), + 'orderby' => 'date_gmt', + 'order' => 'DESC', + 'order_id' => false, + 'limit' => -1, + ) ); + + $where = ' WHERE 1=1'; + + if ( 'any' !== $args['status'] ) { + $where .= $wpdb->prepare( + ' AND status = %s', + $args['status'] + ); + } + + if ( absint( $args['order_id'] ) ) { + $where .= $wpdb->prepare( ' AND order_id = %d', $args['order_id'] ); + } + + if ( ! empty( $args['date_query'] ) ) { + $date_query = new WP_Date_Query( $args['date_query'], 'date_gmt' ); + $where .= $date_query->get_sql(); + } + + $orderby = $wpdb->prepare( ' ORDER BY %s %s', $args['orderby'], $args['order'] ); + $limit = ( $args['limit'] > 0 ) ? $wpdb->prepare( ' LIMIT %d', $args['limit'] ) : ''; + + $raw_retries = $wpdb->get_results( "SELECT * FROM {$this->get_full_table_name()} $where $orderby $limit" ); + $retries = array(); + + foreach ( $raw_retries as $raw_retry ) { + if ( 'ids' === $return ) { + $retries[ $raw_retry->retry_id ] = $raw_retry->retry_id; + } else { + $retries[ $raw_retry->retry_id ] = new WCS_Retry( array( + 'id' => $raw_retry->retry_id, + 'order_id' => $raw_retry->order_id, + 'status' => $raw_retry->status, + 'date_gmt' => $raw_retry->date_gmt, + 'rule_raw' => json_decode( $raw_retry->rule_raw ), + ) ); + } + } + + return $retries; + } + + /** + * Adds our table column to WP_Date_Query valid columns. + * + * @param array $columns Columns array we want to modify. + * + * @return array + * @since 2.4 + */ + public function add_date_valid_column( $columns ) { + $columns[] = 'date_gmt'; + + return $columns; + } + + /** + * Returns our table name with no prefix. + * + * @return string + * @since 2.4 + */ + public static function get_table_name() { + return self::TABLE_NAME; + } + + /** + * Returns the table name with prefix. + * + * @return string + * @since 2.4 + */ + public static function get_full_table_name() { + global $wpdb; + + return $wpdb->prefix . self::TABLE_NAME; + } +} diff --git a/includes/payment-retry/class-wcs-retry-email.php b/includes/payment-retry/class-wcs-retry-email.php old mode 100755 new mode 100644 index 3aeb68c..c6f97b5 --- a/includes/payment-retry/class-wcs-retry-email.php +++ b/includes/payment-retry/class-wcs-retry-email.php @@ -36,13 +36,8 @@ class WCS_Retry_Email { * @since 2.1 */ public static function add_emails( $email_classes ) { - - require_once( plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'includes/emails/class-wcs-email-customer-payment-retry.php' ); - $email_classes['WCS_Email_Customer_Payment_Retry'] = new WCS_Email_Customer_Payment_Retry(); - - require_once( plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'includes/emails/class-wcs-email-payment-retry.php' ); - $email_classes['WCS_Email_Payment_Retry'] = new WCS_Email_Payment_Retry(); + $email_classes['WCS_Email_Payment_Retry'] = new WCS_Email_Payment_Retry(); return $email_classes; } @@ -113,4 +108,3 @@ class WCS_Retry_Email { } } } -WCS_Retry_Email::init(); diff --git a/includes/payment-retry/class-wcs-retry-hybrid-store.php b/includes/payment-retry/class-wcs-retry-hybrid-store.php new file mode 100644 index 0000000..3388c86 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-hybrid-store.php @@ -0,0 +1,155 @@ +database_store = WCS_Retry_Stores::get_database_store(); + $this->post_store = WCS_Retry_Stores::get_post_store(); + + $migrator_class = apply_filters( 'wcs_retry_retry_migrator_class', 'WCS_Retry_Migrator' ); + $this->migrator = new $migrator_class( $this->post_store, $this->database_store, new WC_Logger() ); + } + + /** + * Save the details of a retry to the database + * + * @param WCS_Retry $retry Retry to save. + * + * @return int the retry's ID + * @since 2.4 + */ + public function save( WCS_Retry $retry ) { + $retry_id = $retry->get_id(); + if ( $this->migrator->should_migrate_entry( $retry_id ) ) { + $retry_id = $this->migrator->migrate_entry( $retry_id ); + } + + return $this->database_store->save( new WCS_Retry( array( + 'id' => $retry_id, + 'order_id' => $retry->get_order_id(), + 'status' => $retry->get_status(), + 'date_gmt' => $retry->get_date_gmt(), + 'rule_raw' => $retry->get_rule()->get_raw_data(), + ) ) ); + } + + /** + * Get the details of a retry from the database, and migrates when necessary. + * + * @param int $retry_id Retry we want to get. + * + * @return WCS_Retry + * @since 2.4 + */ + public function get_retry( $retry_id ) { + if ( $this->migrator->should_migrate_entry( $retry_id ) ) { + $retry_id = $this->migrator->migrate_entry( $retry_id ); + } + + return $this->database_store->get_retry( $retry_id ); + } + + /** + * Deletes a retry. + * + * @param int $retry_id + * + * @return bool + * @since 2.4 + */ + public function delete_retry( $retry_id ) { + if ( $this->migrator->should_migrate_entry( $retry_id ) ) { + return $this->migrator->delete_source_store_entry( $retry_id ); + } + + return $this->database_store->delete_retry( $retry_id ); + } + + /** + * Get a set of retries from the database + * + * @param array $args A set of filters: + * 'status': filter to only retries of a certain status, either 'pending', 'processing', 'failed' or 'complete'. Default: 'any', which will return all retries. + * 'date_query': array of dates to filter retries to those that occur 'after' or 'before' a certain date (or between those two dates). Should be a MySQL formated date/time string. + * 'orderby': Order by which property? + * 'order': Order in ASC/DESC. + * 'order_id': filter retries to those which belong to a certain order ID. + * 'limit': How many retries we want to get. + * @param string $return Defines in which format return the entries. options: + * 'objects': Returns an array of WCS_Retry objects + * 'ids': Returns an array of ids. + * + * @return array An array of WCS_Retry objects or ids. + * @since 2.4 + */ + public function get_retries( $args = array(), $return = 'objects' ) { + $source_store_retries = $this->post_store->get_retries( $args, $return ); + + foreach ( $source_store_retries as $source_store_retry_id => $source_store_retry ) { + if ( $this->migrator->should_migrate_entry( $source_store_retry_id ) ) { + $this->migrator->migrate_entry( $source_store_retry_id ); + } + } + + return $this->database_store->get_retries( $args, $return ); + } + + /** + * Get the IDs of all retries from the database for a given order + * + * @param int $order_id order we want to look for. + * + * @return array + * @since 2.4 + */ + public function get_retry_ids_for_order( $order_id ) { + $source_store_retries = $this->post_store->get_retry_ids_for_order( $order_id ); + + foreach ( $source_store_retries as $source_store_retry_id ) { + if ( $this->migrator->should_migrate_entry( $source_store_retry_id ) ) { + $this->migrator->migrate_entry( $source_store_retry_id ); + } + } + + return $this->database_store->get_retry_ids_for_order( $order_id ); + } + +} diff --git a/includes/payment-retry/class-wcs-retry-migrator.php b/includes/payment-retry/class-wcs-retry-migrator.php new file mode 100644 index 0000000..a364b99 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-migrator.php @@ -0,0 +1,120 @@ +destination_store->get_retry( $retry_id ); + } + + /** + * Gets the item from the source store. + * + * @param int $entry_id + * + * @return WCS_Retry + * @since 2.4 + */ + public function get_source_store_entry( $entry_id ) { + return $this->source_store->get_retry( $entry_id ); + } + + /** + * save the item to the destination store. + * + * @param int $entry_id + * + * @return mixed + * @since 2.4 + */ + public function save_destination_store_entry( $entry_id ) { + $source_retry = $this->get_source_store_entry( $entry_id ); + + return $this->destination_store->save( $source_retry ); + } + + /** + * deletes the item from the source store. + * + * @param int $entry_id + * + * @return bool + * @since 2.4 + */ + public function delete_source_store_entry( $entry_id ) { + return $this->source_store->delete_retry( $entry_id ); + } + + /** + * Add a message to the log + * + * @param int $old_retry_id Old retry id. + * @param int $new_retry_id New retry id. + */ + protected function migrated_entry( $old_retry_id, $new_retry_id ) { + $this->log( sprintf( 'Retry ID %d migrated to %s with ID %d.', $old_retry_id, WCS_Retry_Stores::get_database_store()->get_full_table_name(), $new_retry_id ) ); + } + + /** + * If options exists, we need to run migration. + * + * @since 2.4.1 + * @return bool + */ + public static function needs_migration() { + return apply_filters( self::$needs_migration_option_name, ( 'true' === get_option( self::$needs_migration_option_name ) ) ); + } + + /** + * Sets needs migration option. + * + * @since 2.4.1 + */ + public static function set_needs_migration() { + if ( WCS_Retry_Stores::get_post_store()->get_retries( array( 'limit' => 1 ), 'ids' ) ) { + update_option( self::$needs_migration_option_name, 'true' ); + } else { + delete_option( self::$needs_migration_option_name ); + } + } +} + diff --git a/includes/payment-retry/class-wcs-retry-post-store.php b/includes/payment-retry/class-wcs-retry-post-store.php old mode 100755 new mode 100644 index ff1afcc..3972311 --- a/includes/payment-retry/class-wcs-retry-post-store.php +++ b/includes/payment-retry/class-wcs-retry-post-store.php @@ -109,52 +109,60 @@ class WCS_Retry_Post_Store extends WCS_Retry_Store { } /** + * Deletes a retry. * + * @param int $retry_id + * + * @return bool */ - public function get_retries( $args ) { + public function delete_retry( $retry_id ) { + return wp_delete_post( $retry_id, true ); + } + + /** + * Get a set of retries from the database + * + * @param array $args A set of filters: + * 'status': filter to only retries of a certain status, either 'pending', 'processing', 'failed' or 'complete'. Default: 'any', which will return all retries. + * 'date_query': array of dates to filter retries to those that occur 'after' or 'before' a certain date (or between those two dates). Should be a MySQL formated date/time string. + * 'orderby': Order by which property? + * 'order': Order in ASC/DESC. + * 'order_id': filter retries to those which belong to a certain order ID. + * 'limit': How many retries we want to get. + * @param string $return Defines in which format return the entries. options: + * 'objects': Returns an array of WCS_Retry objects + * 'ids': Returns an array of ids. + * + * @return array An array of WCS_Retry objects or ids. + * @since 2.4 + */ + public function get_retries( $args = array(), $return = 'objects' ) { + $retries = array(); $args = wp_parse_args( $args, array( 'status' => 'any', 'date_query' => array(), + 'orderby' => 'date', + 'order' => 'DESC', + 'order_id' => false, + 'limit' => -1, ) ); $retry_post_ids = get_posts( array( - 'posts_per_page' => -1, + 'posts_per_page' => $args['limit'], 'post_type' => self::$post_type, 'post_status' => $args['status'], 'date_query' => $args['date_query'], 'fields' => 'ids', - 'orderby' => 'date', - 'order' => 'DESC', + 'orderby' => $args['orderby'], + 'order' => $args['order'], + 'post_parent' => $args['order_id'], ) ); - $retries = array(); - foreach ( $retry_post_ids as $retry_post_id ) { - $retries[ $retry_post_id ] = $this->get_retry( $retry_post_id ); + $retries[ $retry_post_id ] = 'ids' === $return ? $retry_post_id : $this->get_retry( $retry_post_id ); } return $retries; } - - /** - * Get the IDs of all retries from the database for a given order - * - * @param int $order_id - * @return array - */ - public function get_retry_ids_for_order( $order_id ) { - - $retry_post_ids = get_posts( array( - 'posts_per_page' => -1, - 'post_type' => self::$post_type, - 'post_status' => 'any', - 'post_parent' => $order_id, - 'fields' => 'ids', - 'orderby' => 'ID', - 'order' => 'ASC', - ) ); - - return $retry_post_ids; - } } diff --git a/includes/payment-retry/class-wcs-retry-rule.php b/includes/payment-retry/class-wcs-retry-rule.php old mode 100755 new mode 100644 diff --git a/includes/payment-retry/class-wcs-retry-rules.php b/includes/payment-retry/class-wcs-retry-rules.php old mode 100755 new mode 100644 diff --git a/includes/payment-retry/class-wcs-retry-stores.php b/includes/payment-retry/class-wcs-retry-stores.php new file mode 100644 index 0000000..823d564 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-stores.php @@ -0,0 +1,82 @@ +init(); + } + + return self::$database_store; + } + + /** + * Get the class used for instantiating retry storage via self::destination_store() + * + * @return string + * @since 2.4 + */ + public static function get_database_store_class() { + return apply_filters( 'wcs_retry_database_store_class', 'WCS_Retry_Database_Store' ); + } + + /** + * Access the object used to interface with the source store. + * + * @return WCS_Retry_Store + * @since 2.4 + */ + public static function get_post_store() { + if ( empty( self::$post_store ) ) { + $class = self::get_post_store_class(); + self::$post_store = new $class(); + self::$post_store->init(); + } + + return self::$post_store; + } + + /** + * Get the class used for instantiating retry storage via self::source_store() + * + * @return string + * @since 2.4 + */ + public static function get_post_store_class() { + return apply_filters( 'wcs_retry_post_store_class', 'WCS_Retry_Post_Store' ); + } +} diff --git a/includes/payment-retry/class-wcs-retry-table-maker.php b/includes/payment-retry/class-wcs-retry-table-maker.php new file mode 100644 index 0000000..fe37006 --- /dev/null +++ b/includes/payment-retry/class-wcs-retry-table-maker.php @@ -0,0 +1,59 @@ +tables = array( + WCS_Retry_Stores::get_database_store()->get_table_name(), + ); + } + + /** + * @param string $table + * + * @return string + * @since 2.4 + */ + protected function get_table_definition( $table ) { + global $wpdb; + $table_name = $wpdb->$table; + $charset_collate = $wpdb->get_charset_collate(); + + switch ( $table ) { + case WCS_Retry_Stores::get_database_store()->get_table_name(): + return " + CREATE TABLE {$table_name} ( + retry_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + order_id BIGINT UNSIGNED NOT NULL, + status varchar(255) NOT NULL, + date_gmt datetime NOT NULL default '0000-00-00 00:00:00', + rule_raw text, + PRIMARY KEY (retry_id), + KEY order_id (order_id) + ) $charset_collate; + "; + default: + return ''; + } + } +} diff --git a/includes/payment-retry/class-wcs-retry.php b/includes/payment-retry/class-wcs-retry.php old mode 100755 new mode 100644 diff --git a/includes/privacy/class-wcs-privacy-background-updater.php b/includes/privacy/class-wcs-privacy-background-updater.php old mode 100755 new mode 100644 index 38fef2f..032b81d --- a/includes/privacy/class-wcs-privacy-background-updater.php +++ b/includes/privacy/class-wcs-privacy-background-updater.php @@ -44,8 +44,8 @@ class WCS_Privacy_Background_Updater { * @since 2.2.20 */ public function schedule_ended_subscription_anonymization() { - if ( false === wc_next_scheduled_action( $this->ended_subscription_anonymization_hook ) ) { - wc_schedule_single_action( time(), $this->ended_subscription_anonymization_hook ); + if ( false === as_next_scheduled_action( $this->ended_subscription_anonymization_hook ) ) { + as_schedule_single_action( time(), $this->ended_subscription_anonymization_hook ); } } @@ -55,7 +55,7 @@ class WCS_Privacy_Background_Updater { * @since 2.2.20 */ protected function unschedule_ended_subscription_anonymization() { - wc_unschedule_action( $this->ended_subscription_anonymization_hook ); + as_unschedule_action( $this->ended_subscription_anonymization_hook ); } /** @@ -67,8 +67,8 @@ class WCS_Privacy_Background_Updater { protected function schedule_subscription_orders_anonymization( $subscription_id ) { $action_args = array( 'subscription_id' => intval( $subscription_id ) ); - if ( false === wc_next_scheduled_action( $this->subscription_orders_anonymization_hook, $action_args ) ) { - wc_schedule_single_action( time(), $this->subscription_orders_anonymization_hook, $action_args ); + if ( false === as_next_scheduled_action( $this->subscription_orders_anonymization_hook, $action_args ) ) { + as_schedule_single_action( time(), $this->subscription_orders_anonymization_hook, $action_args ); } } @@ -79,7 +79,7 @@ class WCS_Privacy_Background_Updater { * @param int The subscription ID. */ protected function unschedule_subscription_orders_anonymization( $subscription_id ) { - wc_unschedule_action( $this->subscription_orders_anonymization_hook, array( 'subscription_id' => intval( $subscription_id ) ) ); + as_unschedule_action( $this->subscription_orders_anonymization_hook, array( 'subscription_id' => intval( $subscription_id ) ) ); } /** @@ -89,7 +89,7 @@ class WCS_Privacy_Background_Updater { * @param int The order ID. */ protected function schedule_order_anonymization( $order_id ) { - wc_schedule_single_action( time(), $this->order_anonymization_hook, array( 'order_id' => intval( $order_id ) ) ); + as_schedule_single_action( time(), $this->order_anonymization_hook, array( 'order_id' => intval( $order_id ) ) ); } /** @@ -100,7 +100,7 @@ class WCS_Privacy_Background_Updater { * @return bool Wether the order has a scheduled anonymization action. */ protected function order_anonymization_is_scheduled( $order_id ) { - return false !== wc_next_scheduled_action( $this->order_anonymization_hook, array( 'order_id' => intval( $order_id ) ) ); + return false !== as_next_scheduled_action( $this->order_anonymization_hook, array( 'order_id' => intval( $order_id ) ) ); } /** diff --git a/includes/privacy/class-wcs-privacy-erasers.php b/includes/privacy/class-wcs-privacy-erasers.php old mode 100755 new mode 100644 diff --git a/includes/privacy/class-wcs-privacy-exporters.php b/includes/privacy/class-wcs-privacy-exporters.php old mode 100755 new mode 100644 diff --git a/includes/privacy/class-wcs-privacy.php b/includes/privacy/class-wcs-privacy.php old mode 100755 new mode 100644 index 462e80f..81e650f --- a/includes/privacy/class-wcs-privacy.php +++ b/includes/privacy/class-wcs-privacy.php @@ -39,10 +39,7 @@ class WCS_Privacy extends WC_Abstract_Privacy { parent::__construct( __( 'WooCommerce Subscriptions', 'woocommerce-subscriptions' ) ); - // include our exporters and erasers. - include_once 'class-wcs-privacy-erasers.php'; - include_once 'class-wcs-privacy-exporters.php'; - + // Add our exporters and erasers. $this->add_exporter( 'woocommerce-subscriptions-data', __( 'Subscriptions Data', 'woocommerce-subscriptions' ), array( 'WCS_Privacy_Exporters', 'subscription_data_exporter' ) ); $this->add_eraser( 'woocommerce-subscriptions-data', __( 'Subscriptions Data', 'woocommerce-subscriptions' ), array( 'WCS_Privacy_Erasers', 'subscription_data_eraser' ) ); } @@ -214,10 +211,15 @@ class WCS_Privacy extends WC_Abstract_Privacy { return $settings; } + $erasure_text = esc_html__( 'account erasure request', 'woocommerce-subscriptions' ); + if ( current_user_can( 'manage_privacy_options' ) ) { + $erasure_text = sprintf( '%s', esc_url( admin_url( 'tools.php?page=remove_personal_data' ) ), $erasure_text ); + } + WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_erasure_request_removes_order_data', array( 'desc' => __( 'Remove personal data from subscriptions', 'woocommerce-subscriptions' ), - /* Translators: placeholders are opening and closing link tags linking to the erasure request screen. */ - 'desc_tip' => sprintf( __( 'When handling an %saccount erasure request%s, should personal data within subscriptions be retained or removed?', 'woocommerce-subscriptions' ), '' , '' ), + /* Translators: %s URL to erasure request screen. */ + 'desc_tip' => sprintf( __( 'When handling an %s, should personal data within subscriptions be retained or removed?', 'woocommerce-subscriptions' ), $erasure_text ), 'id' => 'woocommerce_erasure_request_removes_subscription_data', 'type' => 'checkbox', 'default' => 'no', diff --git a/includes/upgrades/class-wc-subscriptions-upgrader.php b/includes/upgrades/class-wc-subscriptions-upgrader.php old mode 100755 new mode 100644 index c2e6cf8..fa54ae9 --- a/includes/upgrades/class-wc-subscriptions-upgrader.php +++ b/includes/upgrades/class-wc-subscriptions-upgrader.php @@ -101,6 +101,9 @@ class WC_Subscriptions_Upgrader { // When WC is updated from a version prior to 3.0 to a version after 3.0, add subscription address indexes. Must be hooked on before WC runs its updates, which occur on priority 5. add_action( 'init', array( __CLASS__, 'maybe_add_subscription_address_indexes' ), 2 ); + // Hooks into WC's wc_update_350_order_customer_id upgrade routine. + add_action( 'init', array( __CLASS__, 'maybe_update_subscription_post_author' ), 2 ); + add_action( 'admin_notices', array( __CLASS__, 'maybe_add_downgrade_notice' ) ); add_action( 'admin_notices', array( __CLASS__, 'maybe_display_external_object_cache_warning' ) ); @@ -153,6 +156,11 @@ class WC_Subscriptions_Upgrader { update_option( WC_Subscriptions_Admin::$option_prefix . '_previous_version', self::$active_version ); + /** + * before upgrade hook. + */ + do_action( 'woocommerce_subscriptions_before_upgrade', WC_Subscriptions::$version, self::$active_version ); + // Update the hold stock notification to be one week (if it's still at the default 60 minutes) to prevent cancelling subscriptions using manual renewals and payment methods that can take more than 1 hour (i.e. PayPal eCheck) if ( '0' == self::$active_version || version_compare( self::$active_version, '1.4', '<' ) ) { @@ -191,10 +199,7 @@ class WC_Subscriptions_Upgrader { // Delete cached subscription length ranges to force an update with 2.1 WC_Subscriptions::$cache->delete_cached( 'wcs-sub-ranges-' . get_locale() ); - WCS_Upgrade_Logger::add( 'v2.1: Deleted cached subscription ranges.' ); - - include_once( 'class-wcs-upgrade-2-1.php' ); WCS_Upgrade_2_1::set_cancelled_dates(); // Schedule report cache updates in the hopes that the data is ready and waiting for the store owner the first time they visit the reports pages @@ -203,13 +208,11 @@ class WC_Subscriptions_Upgrader { // Repair missing end_of_prepaid_term scheduled actions if ( version_compare( self::$active_version, '2.2.0', '>=' ) && version_compare( self::$active_version, '2.2.7', '<' ) ) { - include_once( 'class-wcs-upgrade-2-2-7.php' ); WCS_Upgrade_2_2_7::schedule_end_of_prepaid_term_repair(); } // Repair missing _contains_synced_subscription post meta if ( version_compare( get_option( 'woocommerce_db_version' ), '3.0', '>=' ) && version_compare( self::$active_version, '2.2.0', '>=' ) && version_compare( self::$active_version, '2.2.9', '<' ) ) { - include_once( 'class-wcs-upgrade-2-2-9.php' ); WCS_Upgrade_2_2_9::schedule_repair(); } @@ -231,6 +234,10 @@ class WC_Subscriptions_Upgrader { } } + if ( version_compare( self::$active_version, '2.4.0', '<' ) ) { + self::$background_updaters['2.4']['start_date_metadata']->schedule_repair(); + } + self::upgrade_complete(); } @@ -325,9 +332,6 @@ class WC_Subscriptions_Upgrader { break; case 'products': - - require_once( 'class-wcs-upgrade-1-5.php' ); - $upgraded_product_count = WCS_Upgrade_1_5::upgrade_products(); $results = array( // translators: placeholder is number of upgraded subscriptions @@ -336,9 +340,6 @@ class WC_Subscriptions_Upgrader { break; case 'hooks': - - require_once( 'class-wcs-upgrade-1-5.php' ); - $upgraded_hook_count = WCS_Upgrade_1_5::upgrade_hooks( self::$upgrade_limit_hooks ); $results = array( 'upgraded_count' => $upgraded_hook_count, @@ -348,10 +349,6 @@ class WC_Subscriptions_Upgrader { break; case 'subscriptions': - - require_once( 'class-wcs-repair-2-0.php' ); - require_once( 'class-wcs-upgrade-2-0.php' ); - try { $upgraded_subscriptions = WCS_Upgrade_2_0::upgrade_subscriptions( self::$upgrade_limit_subscriptions ); @@ -380,10 +377,6 @@ class WC_Subscriptions_Upgrader { break; case 'subscription_dates_repair': - - require_once( 'class-wcs-upgrade-2-0.php' ); - require_once( 'class-wcs-repair-2-0-2.php' ); - $subscription_ids_to_repair = WCS_Repair_2_0_2::get_subscriptions_to_repair( self::$upgrade_limit_subscriptions ); try { @@ -459,7 +452,7 @@ class WC_Subscriptions_Upgrader { private static function upgrade_really_old_versions() { if ( '0' != self::$active_version && version_compare( self::$active_version, '1.2', '<' ) ) { - include_once( 'class-wcs-upgrade-1-2.php' ); + WCS_Upgrade_1_2::init(); self::generate_renewal_orders(); update_option( WC_Subscriptions_Admin::$option_prefix . '_active_version', '1.2' ); $upgraded_versions = '1.2, '; @@ -467,14 +460,14 @@ class WC_Subscriptions_Upgrader { // Add Variable Subscription product type term if ( '0' != self::$active_version && version_compare( self::$active_version, '1.3', '<' ) ) { - include_once( 'class-wcs-upgrade-1-3.php' ); + WCS_Upgrade_1_3::init(); update_option( WC_Subscriptions_Admin::$option_prefix . '_active_version', '1.3' ); $upgraded_versions .= '1.3 & '; } // Moving subscription meta out of user meta and into item meta if ( '0' != self::$active_version && version_compare( self::$active_version, '1.4', '<' ) ) { - include_once( 'class-wcs-upgrade-1-4.php' ); + WCS_Upgrade_1_4::init(); update_option( WC_Subscriptions_Admin::$option_prefix . '_active_version', '1.4' ); $upgraded_versions .= '1.4.'; } @@ -604,7 +597,7 @@ class WC_Subscriptions_Upgrader { $about_page_url = self::$about_page_url; @header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); - include_once( 'templates/wcs-upgrade.php' ); + include_once( dirname( __FILE__ ) . '/templates/wcs-upgrade.php' ); WCS_Upgrade_Logger::add( 'Loaded database upgrade helper' ); } @@ -617,7 +610,7 @@ class WC_Subscriptions_Upgrader { * @since 1.4 */ public static function upgrade_in_progress_notice() { - include_once( 'templates/wcs-upgrade-in-progress.php' ); + include_once( dirname( __FILE__ ) . '/templates/wcs-upgrade-in-progress.php' ); WCS_Upgrade_Logger::add( 'Loaded database upgrade in progress notice...' ); } @@ -660,7 +653,7 @@ class WC_Subscriptions_Upgrader { $active_version = self::$active_version; $settings_page = admin_url( 'admin.php?page=wc-settings&tab=subscriptions' ); - include_once( 'templates/wcs-about.php' ); + include_once( dirname( __FILE__ ) . '/templates/wcs-about.php' ); } /** @@ -783,7 +776,6 @@ class WC_Subscriptions_Upgrader { * @since 2.2.7 */ public static function repair_end_of_prepaid_term_actions() { - include_once( 'class-wcs-upgrade-2-2-7.php' ); WCS_Upgrade_2_2_7::repair_pending_cancelled_subscriptions(); } @@ -793,7 +785,6 @@ class WC_Subscriptions_Upgrader { * @since 2.2.9 */ public static function repair_subscription_contains_sync_meta() { - include_once( 'class-wcs-upgrade-2-2-9.php' ); WCS_Upgrade_2_2_9::repair_subscriptions_containing_synced_variations(); } @@ -810,11 +801,12 @@ class WC_Subscriptions_Upgrader { } $admin_notice = new WCS_Admin_Notice( 'error' ); - $admin_notice->set_simple_content( sprintf( esc_html__( '%1$sWarning!%2$s It appears that you have downgraded %1$sWooCommerce Subscriptions%2$s from %3$s to %4$s. Downgrading the plugin in this way may cause issues. Please update to %3$s or higher, or %5$sopen a new support ticket%6$s for further assistance.', 'woocommerce-subscriptions' ), + $admin_notice->set_simple_content( sprintf( esc_html__( '%1$sWarning!%2$s It appears that you have downgraded %1$sWooCommerce Subscriptions%2$s from %3$s to %4$s. Downgrading the plugin in this way may cause issues. Please update to %3$s or higher, or %5$sopen a new support ticket%6$s for further assistance. %7$sLearn more »%8$s', 'woocommerce-subscriptions' ), '', '', '' . self::$active_version . '', '' . WC_Subscriptions::$version . '', - '', '' + '', '', + '', '' ) ); $admin_notice->display(); @@ -834,19 +826,35 @@ class WC_Subscriptions_Upgrader { } } + /** + * Handles the WC 3.5.0 upgrade routine that moves customer IDs from post metadata to the 'post_author' column. + * + * @since 2.4.0 + */ + public static function maybe_update_subscription_post_author() { + if ( version_compare( WC()->version, '3.5.0', '<' ) ) { + return; + } + + // If WC hasn't run the update routine yet we can hook into theirs to update subscriptions, otherwise we'll need to schedule our own update. + if ( version_compare( get_option( 'woocommerce_db_version' ), '3.5.0', '<' ) ) { + self::$background_updaters['2.4']['subscription_post_author']->hook_into_wc_350_update(); + } else if ( version_compare( self::$active_version, '2.4.0', '<' ) ) { + self::$background_updaters['2.4']['subscription_post_author']->schedule_repair(); + } + } + /** * Load and initialise the background updaters. * - * @since 2.3.0 + * @since 2.4.0 */ public static function initialise_background_updaters() { $logger = new WC_logger(); - - include_once( dirname( __FILE__ ) . '/class-wcs-repair-suspended-paypal-subscriptions.php' ); - include_once( dirname( __FILE__ ) . '/class-wcs-repair-subscription-address-indexes.php' ); - self::$background_updaters['2.3']['suspended_paypal_repair'] = new WCS_Repair_Suspended_PayPal_Subscriptions( $logger ); self::$background_updaters['2.3']['address_indexes_repair'] = new WCS_Repair_Subscription_Address_Indexes( $logger ); + self::$background_updaters['2.4']['start_date_metadata'] = new WCS_Repair_Start_Date_Metadata( $logger ); + self::$background_updaters['2.4']['subscription_post_author'] = new WCS_Upgrade_Subscription_Post_Author( $logger ); // Init the updaters foreach ( self::$background_updaters as $version => $updaters ) { @@ -908,4 +916,3 @@ class WC_Subscriptions_Upgrader { return WCS_Upgrade_1_4::is_user_upgraded( $user_id ); } } -add_action( 'after_setup_theme', 'WC_Subscriptions_Upgrader::init', 11 ); diff --git a/includes/upgrades/class-wcs-repair-2-0-2.php b/includes/upgrades/class-wcs-repair-2-0-2.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/class-wcs-repair-2-0.php b/includes/upgrades/class-wcs-repair-2-0.php old mode 100755 new mode 100644 index e236296..3892a08 --- a/includes/upgrades/class-wcs-repair-2-0.php +++ b/includes/upgrades/class-wcs-repair-2-0.php @@ -595,7 +595,7 @@ class WCS_Repair_2_0 { WCS_Upgrade_Logger::add( sprintf( '-- For order %d: Repairing date type "%s" from action scheduler...', $subscription['order_id'], $type ) ); WCS_Upgrade_Logger::add( '-- This is the arguments: ' . PHP_EOL . print_r( array( $action_args, 'hook' => $type ), true ) . PHP_EOL ); - $next_date_timestamp = wc_next_scheduled_action( $type, $action_args ); + $next_date_timestamp = as_next_scheduled_action( $type, $action_args ); if ( false === $next_date_timestamp ) { // set it to 0 as default diff --git a/includes/upgrades/class-wcs-repair-start-date-metadata.php b/includes/upgrades/class-wcs-repair-start-date-metadata.php new file mode 100644 index 0000000..42af3d0 --- /dev/null +++ b/includes/upgrades/class-wcs-repair-start-date-metadata.php @@ -0,0 +1,70 @@ +scheduled_hook = 'wcs_add_start_date_metadata'; + $this->log_handle = 'wcs-add-start-date-metadata'; + $this->logger = $logger; + } + + /** + * Update a subscription, saving its start date as metadata. + * + * @since 2.4.0 + */ + protected function update_item( $subscription_id ) { + try { + $subscription = wcs_get_subscription( $subscription_id ); + + if ( false === $subscription ) { + throw new Exception( 'Failed to instantiate subscription object' ); + } + + // Saving the subscription is enough to save the start date. + $subscription->save(); + + $this->log( sprintf( 'Subscription ID %d start date metadata added.', $subscription_id ) ); + } catch ( Exception $e ) { + $this->log( sprintf( '--- Exception caught adding start date metadata to subscription %d - exception message: %s ---', $subscription_id, $e->getMessage() ) ); + } + } + + /** + * Get a batch of subscriptions to repair. + * + * @since 2.4.0 + * @return array A list of subscription ids which may need to be repaired. + */ + protected function get_items_to_update() { + global $wpdb; + + return $wpdb->get_col( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'shop_subscription' + AND post_status NOT IN ( 'trash', 'auto-draft' ) + AND ID NOT IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_schedule_start' ) + LIMIT 20" + ); + } + +} diff --git a/includes/upgrades/class-wcs-repair-subscription-address-indexes.php b/includes/upgrades/class-wcs-repair-subscription-address-indexes.php old mode 100755 new mode 100644 index 8c21059..5527026 --- a/includes/upgrades/class-wcs-repair-subscription-address-indexes.php +++ b/includes/upgrades/class-wcs-repair-subscription-address-indexes.php @@ -21,10 +21,11 @@ class WCS_Repair_Subscription_Address_Indexes extends WCS_Background_Upgrader { /** * Constructor * - * @param WC_Logger $logger The WC_Logger instance. + * @param WC_Logger_Interface $logger The WC_Logger instance. + * * @since 2.3.0 */ - public function __construct( WC_Logger $logger ) { + public function __construct( WC_Logger_Interface $logger ) { $this->scheduled_hook = 'wcs_add_missing_subscription_address_indexes'; $this->log_handle = 'wcs-add-subscription-address-indexes'; $this->logger = $logger; diff --git a/includes/upgrades/class-wcs-repair-suspended-paypal-subscriptions.php b/includes/upgrades/class-wcs-repair-suspended-paypal-subscriptions.php old mode 100755 new mode 100644 index e9b1631..a3f6255 --- a/includes/upgrades/class-wcs-repair-suspended-paypal-subscriptions.php +++ b/includes/upgrades/class-wcs-repair-suspended-paypal-subscriptions.php @@ -24,11 +24,11 @@ class WCS_Repair_Suspended_PayPal_Subscriptions extends WCS_Background_Upgrader /** * Constructor. * - * @param WC_Logger $logger The WC Logger instance. + * @param WC_Logger_Interface $logger The WC Logger instance. * * @since 2.3.0 */ - public function __construct( WC_Logger $logger ) { + public function __construct( WC_Logger_Interface $logger ) { $this->scheduled_hook = 'wcs_repair_subscriptions_suspended_paypal_not_woocommerce'; $this->log_handle = 'wcs-upgrade-subscriptions-paypal-suspended'; $this->logger = $logger; diff --git a/includes/upgrades/class-wcs-upgrade-1-2.php b/includes/upgrades/class-wcs-upgrade-1-2.php old mode 100755 new mode 100644 index 84829fd..643d8fc --- a/includes/upgrades/class-wcs-upgrade-1-2.php +++ b/includes/upgrades/class-wcs-upgrade-1-2.php @@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) { class WCS_Upgrade_1_2 { - public function init() { + public static function init() { global $wpdb; // Get IDs only and use a direct DB query for efficiency @@ -276,4 +276,3 @@ class WCS_Upgrade_1_2 { } } } -WCS_Upgrade_1_2::init(); diff --git a/includes/upgrades/class-wcs-upgrade-1-3.php b/includes/upgrades/class-wcs-upgrade-1-3.php old mode 100755 new mode 100644 index b3036dd..7c25bc6 --- a/includes/upgrades/class-wcs-upgrade-1-3.php +++ b/includes/upgrades/class-wcs-upgrade-1-3.php @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { class WCS_Upgrade_1_3 { - public function init() { + public static function init() { global $wpdb; // Change transient timeout entries to be a vanilla option @@ -36,4 +36,3 @@ class WCS_Upgrade_1_3 { OR option_name LIKE '_transient_block_scheduled_subscription_payments_%'" ); } } -WCS_Upgrade_1_3::init(); diff --git a/includes/upgrades/class-wcs-upgrade-1-4.php b/includes/upgrades/class-wcs-upgrade-1-4.php old mode 100755 new mode 100644 index df0ee2f..679fd1a --- a/includes/upgrades/class-wcs-upgrade-1-4.php +++ b/includes/upgrades/class-wcs-upgrade-1-4.php @@ -18,7 +18,7 @@ class WCS_Upgrade_1_4 { private static $last_upgraded_user_id = false; - public function init() { + public static function init() { global $wpdb; @@ -177,4 +177,3 @@ class WCS_Upgrade_1_4 { return $user_id > self::$last_upgraded_user_id; } } -WCS_Upgrade_1_4::init(); diff --git a/includes/upgrades/class-wcs-upgrade-1-5.php b/includes/upgrades/class-wcs-upgrade-1-5.php old mode 100755 new mode 100644 index d855b57..5eb8aec --- a/includes/upgrades/class-wcs-upgrade-1-5.php +++ b/includes/upgrades/class-wcs-upgrade-1-5.php @@ -67,8 +67,8 @@ class WCS_Upgrade_1_5 { if ( 'scheduled_subscription_payment' == $hook || 'scheduled_subscription_expiration' == $hook || 'scheduled_subscription_end_of_prepaid_term' == $hook || 'scheduled_subscription_trial_end' == $hook || 'paypal_check_subscription_payment' == $hook ) { foreach ( $details as $hook_key => $values ) { - if ( ! wc_next_scheduled_action( $hook, $values['args'] ) ) { - wc_schedule_single_action( $timestamp, $hook, $values['args'] ); + if ( ! as_next_scheduled_action( $hook, $values['args'] ) ) { + as_schedule_single_action( $timestamp, $hook, $values['args'] ); unset( $cron[ $timestamp ][ $hook ][ $hook_key ] ); $counter++; } diff --git a/includes/upgrades/class-wcs-upgrade-2-0.php b/includes/upgrades/class-wcs-upgrade-2-0.php old mode 100755 new mode 100644 index e865410..a23fdcd --- a/includes/upgrades/class-wcs-upgrade-2-0.php +++ b/includes/upgrades/class-wcs-upgrade-2-0.php @@ -68,7 +68,7 @@ class WCS_Upgrade_2_0 { $original_order = wc_get_order( $old_subscription['order_id'] ); // If we're still in a prepaid term, the new subscription has the new pending cancellation status - if ( 'cancelled' == $old_subscription['status'] && false != wc_next_scheduled_action( 'scheduled_subscription_end_of_prepaid_term', array( 'user_id' => $old_subscription['user_id'], 'subscription_key' => $old_subscription['subscription_key'] ) ) ) { + if ( 'cancelled' == $old_subscription['status'] && false != as_next_scheduled_action( 'scheduled_subscription_end_of_prepaid_term', array( 'user_id' => $old_subscription['user_id'], 'subscription_key' => $old_subscription['subscription_key'] ) ) ) { $subscription_status = 'pending-cancel'; } elseif ( 'trash' == $old_subscription['status'] ) { $subscription_status = 'cancelled'; // we'll trash it properly after migrating it @@ -501,12 +501,12 @@ class WCS_Upgrade_2_0 { } elseif ( ! empty( $old_keys['old_scheduled_hook'] ) ) { // Now check if there is a scheduled date, this is for next payment and end of prepaid term dates - $next_scheduled = wc_next_scheduled_action( $old_keys['old_scheduled_hook'], $old_hook_args ); + $next_scheduled = as_next_scheduled_action( $old_keys['old_scheduled_hook'], $old_hook_args ); if ( $next_scheduled > 0 ) { if ( 'end_of_prepaid_term' == $new_key ) { - wc_schedule_single_action( $next_scheduled, 'woocommerce_scheduled_subscription_end_of_prepaid_term', array( 'subscription_id' => $new_subscription->get_id() ) ); + as_schedule_single_action( $next_scheduled, 'woocommerce_scheduled_subscription_end_of_prepaid_term', array( 'subscription_id' => $new_subscription->get_id() ) ); } else { $dates_to_update[ $new_key ] = gmdate( 'Y-m-d H:i:s', $next_scheduled ); } diff --git a/includes/upgrades/class-wcs-upgrade-2-1.php b/includes/upgrades/class-wcs-upgrade-2-1.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/class-wcs-upgrade-2-2-7.php b/includes/upgrades/class-wcs-upgrade-2-2-7.php old mode 100755 new mode 100644 index 0d808e7..0761806 --- a/includes/upgrades/class-wcs-upgrade-2-2-7.php +++ b/includes/upgrades/class-wcs-upgrade-2-2-7.php @@ -60,12 +60,12 @@ class WCS_Upgrade_2_2_7 { $subscription->update_status( 'cancelled', __( 'Subscription end date in the past', 'woocommerce-subscriptions' ) ); } else { $action_args = array( 'subscription_id' => $subscription_id ); - $scheduled_action = wc_next_scheduled_action( $end_of_prepaid_term_hook, $action_args ); + $scheduled_action = as_next_scheduled_action( $end_of_prepaid_term_hook, $action_args ); // If there isn't a scheduled end of prepaid term, schedule one now. if ( false == $scheduled_action ) { self::log( sprintf( 'Subscription %d missing scheduled end of prepaid term action - scheduled new action (end timestamp: %d)', $subscription_id, $end_time ) ); - wc_schedule_single_action( $end_time, $end_of_prepaid_term_hook, $action_args ); + as_schedule_single_action( $end_time, $end_of_prepaid_term_hook, $action_args ); } else { self::log( sprintf( 'Subscription %d has a scheduled end of prepaid term action - there\'s nothing to do here', $subscription_id ) ); } diff --git a/includes/upgrades/class-wcs-upgrade-2-2-9.php b/includes/upgrades/class-wcs-upgrade-2-2-9.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/class-wcs-upgrade-logger.php b/includes/upgrades/class-wcs-upgrade-logger.php old mode 100755 new mode 100644 index 872bb17..61132fd --- a/includes/upgrades/class-wcs-upgrade-logger.php +++ b/includes/upgrades/class-wcs-upgrade-logger.php @@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { class WCS_Upgrade_Logger { - /** @var WC_Logger instance */ + /** @var WC_Logger_Interface instance */ protected static $log = false; /** @var string File handle */ @@ -71,4 +71,3 @@ class WCS_Upgrade_Logger { self::add( sprintf( '%s upgrade complete from Subscriptions v%s while WooCommerce WC_VERSION %s and database version %s was active.', $current_version, $old_version, $wc_version, get_option( 'woocommerce_db_version' ) ) ); } } -WCS_Upgrade_Logger::init(); diff --git a/includes/upgrades/class-wcs-upgrade-notice-manager.php b/includes/upgrades/class-wcs-upgrade-notice-manager.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/class-wcs-upgrade-subscription-post-author.php b/includes/upgrades/class-wcs-upgrade-subscription-post-author.php new file mode 100644 index 0000000..89c3a9c --- /dev/null +++ b/includes/upgrades/class-wcs-upgrade-subscription-post-author.php @@ -0,0 +1,165 @@ +scheduled_hook = 'wcs_upgrade_subscription_post_author'; + $this->log_handle = 'wcs-upgrade-subscription-post-author'; + $this->logger = $logger; + } + + /** + * Update a subscription, setting its post_author to its customer ID. + * + * @since 2.4.0 + */ + protected function update_item( $subscription_id ) { + global $wpdb; + + try { + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->posts} p + INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + SET p.post_author = pm.meta_value WHERE p.ID = %d AND pm.meta_key = '_customer_user'", + $subscription_id + ) + ); + + if ( 0 === $wpdb->rows_affected ) { + if ( '1' === get_post_meta( $subscription_id, '_customer_user', true ) && is_a( WCS_Customer_Store::instance(), 'WCS_Customer_Store_Cached_CPT' ) ) { + // Admin's subscription cache seems to be corrupt, force a refresh. + WCS_Customer_Store::instance()->delete_cache_for_user( 1 ); + } + + throw new Exception( 'post_author wasn\'t updated, it was already set to 1' ); + } + + $this->log( sprintf( 'Subscription ID %d post_author updated.', $subscription_id ) ); + } catch ( Exception $e ) { + $this->log( sprintf( '--- Exception caught repairing subscription %d - exception message: %s ---', $subscription_id, $e->getMessage() ) ); + + // Ignore this subscription the next time around. + $this->add_subscription_to_ignore_list( $subscription_id ); + } + } + + + /** + * Get a batch of subscriptions which need to be updated. + * + * @since 2.4.0 + * @return array A list of subscription ids which need to be updated. + */ + protected function get_items_to_update() { + return get_posts( array( + 'post_type' => 'shop_subscription', + 'posts_per_page' => 20, + 'author' => '1', + 'post_status' => 'any', + 'post__not_in' => $this->get_subscriptions_to_ignore(), + 'fields' => 'ids', + ) ); + } + + /** + * Schedule the instance's hook to run in $this->time_limit seconds, if it's not already scheduled. + */ + protected function schedule_background_update() { + parent::schedule_background_update(); + + update_option( 'wcs_subscription_post_author_upgrade_is_scheduled', true ); + } + + /** + * Unschedule the instance's hook in Action Scheduler + */ + protected function unschedule_background_updates() { + parent::unschedule_background_updates(); + + delete_option( 'wcs_subscription_post_author_upgrade_is_scheduled' ); + delete_option( 'wcs_post_author_upgrade_other_subscriptions_to_ignore' ); + } + + /** + * Returns the list of admin subscription IDs to ignore during this upgrade routine. + * + * @return array + */ + private function get_subscriptions_to_ignore() { + $subscriptions_to_ignore = WCS_Customer_Store::instance()->get_users_subscription_ids( 1 ); + $other_subscriptions_to_ignore = get_option( 'wcs_post_author_upgrade_other_subscriptions_to_ignore', array() ); + + if ( is_array( $other_subscriptions_to_ignore ) && ! empty( $other_subscriptions_to_ignore ) ) { + $subscriptions_to_ignore = array_unique( array_merge( $subscriptions_to_ignore, $other_subscriptions_to_ignore ) ); + } + + return $subscriptions_to_ignore; + } + + /** + * Adds a subscription ID to the ignore list for this upgrade routine. + * + * @param int $subscription_id + */ + private function add_subscription_to_ignore_list( $subscription_id ) { + $subscription_id = absint( $subscription_id ); + if ( ! $subscription_id ) { + return; + } + + $subscriptions_to_ignore = get_option( 'wcs_post_author_upgrade_other_subscriptions_to_ignore', array() ); + $subscriptions_to_ignore = is_array( $subscriptions_to_ignore ) ? $subscriptions_to_ignore : array(); + + if ( ! in_array( $subscription_id, $subscriptions_to_ignore ) ) { + $subscriptions_to_ignore[] = $subscription_id; + } + + update_option( 'wcs_post_author_upgrade_other_subscriptions_to_ignore', $subscriptions_to_ignore ); + } + + /** + * Hooks into WC's 3.5 update routine to add the subscription post type to the list of post types affected by this update. + * + * @since 2.4.0 + */ + public static function hook_into_wc_350_update() { + add_filter( 'woocommerce_update_350_order_customer_id_post_types', array( __CLASS__, 'add_post_type_to_wc_350_update' ) ); + } + + /** + * Callback for the `woocommerce_update_350_order_customer_id_post_types` hook. Makes sure `shop_subscription` is + * included in the post types array. + * + * @param array $post_types + * @return array + * @since 2.4.0 + */ + public static function add_post_type_to_wc_350_update( $post_types = array() ) { + if ( ! in_array( 'shop_subscription', $post_types ) ) { + $post_types[] = 'shop_subscription'; + } + + return $post_types; + } + +} + diff --git a/includes/upgrades/templates/update-welcome-notice.php b/includes/upgrades/templates/update-welcome-notice.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/templates/wcs-about-2-0.php b/includes/upgrades/templates/wcs-about-2-0.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/templates/wcs-about.php b/includes/upgrades/templates/wcs-about.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/templates/wcs-upgrade-in-progress.php b/includes/upgrades/templates/wcs-upgrade-in-progress.php old mode 100755 new mode 100644 diff --git a/includes/upgrades/templates/wcs-upgrade.php b/includes/upgrades/templates/wcs-upgrade.php old mode 100755 new mode 100644 diff --git a/includes/wcs-cart-functions.php b/includes/wcs-cart-functions.php old mode 100755 new mode 100644 index 279494b..f263017 --- a/includes/wcs-cart-functions.php +++ b/includes/wcs-cart-functions.php @@ -382,3 +382,21 @@ function wcs_get_cart_item_name( $cart_item, $include = array() ) { return $cart_item_name; } + +/** + * Allows protected products to be renewed. + * + * @since 2.4.0 + */ +function wcs_allow_protected_products_to_renew() { + remove_filter( 'woocommerce_add_to_cart_validation', 'wc_protected_product_add_to_cart' ); +} + +/** + * Restores protected products from being added to the cart. + * @see wcs_allow_protected_products_to_renew + * @since 2.4.0 + */ +function wcs_disallow_protected_product_add_to_cart_validation() { + add_filter( 'woocommerce_add_to_cart_validation', 'wc_protected_product_add_to_cart', 10, 2 ); +} diff --git a/includes/wcs-compatibility-functions.php b/includes/wcs-compatibility-functions.php old mode 100755 new mode 100644 diff --git a/includes/wcs-conditional-functions.php b/includes/wcs-conditional-functions.php old mode 100755 new mode 100644 diff --git a/includes/wcs-deprecated-functions.php b/includes/wcs-deprecated-functions.php old mode 100755 new mode 100644 index 9dc2dc2..6a0a7ec --- a/includes/wcs-deprecated-functions.php +++ b/includes/wcs-deprecated-functions.php @@ -237,7 +237,7 @@ function wcs_get_subscription_in_deprecated_structure( WC_Subscription $subscrip 'length' => wcs_estimate_periods_between( ( 0 == $subscription->get_time( 'trial_end' ) ) ? $subscription->get_time( 'date_created' ) : $subscription->get_time( 'trial_end' ), $subscription->get_time( 'end' ) + 120, $subscription->get_billing_period(), 'floor' ) / $subscription->get_billing_interval(), // Since subscriptions no longer have a length, we need to calculate the length given the start and end dates and the period. // Subscription dates - 'start_date' => $subscription->get_date( 'date_created' ), + 'start_date' => $subscription->get_date( 'start' ), 'expiry_date' => $subscription->get_date( 'end' ), 'end_date' => $subscription->has_status( wcs_get_subscription_ended_statuses() ) ? $subscription->get_date( 'end' ) : 0, 'trial_expiry_date' => $subscription->get_date( 'trial_end' ), diff --git a/includes/wcs-formatting-functions.php b/includes/wcs-formatting-functions.php old mode 100755 new mode 100644 diff --git a/includes/wcs-helper-functions.php b/includes/wcs-helper-functions.php old mode 100755 new mode 100644 diff --git a/includes/wcs-limit-functions.php b/includes/wcs-limit-functions.php old mode 100755 new mode 100644 diff --git a/includes/wcs-order-functions.php b/includes/wcs-order-functions.php old mode 100755 new mode 100644 index 105cfef..e4c4259 --- a/includes/wcs-order-functions.php +++ b/includes/wcs-order-functions.php @@ -540,13 +540,16 @@ function wcs_get_subscription_orders( $return_fields = 'ids', $order_type = 'par } /** - * A wrapper for getting a specific item from a subscription. + * A wrapper for getting a specific item from an order or subscription. * * WooCommerce has a wc_add_order_item() function, wc_update_order_item() function and wc_delete_order_item() function, * but no `wc_get_order_item()` function, so we need to add our own (for now). * - * @param int $item_id The ID of an order item - * @return WC_Subscription Subscription details in post_id => WC_Subscription form. + * @param int $item_id The ID of an order item + * @param WC_Order|WC_Subscription $order The order or order object the item belongs to. + * + * @return WC_Order_Item|array The order item object or an empty array if the item doesn't exist. + * * @since 2.0 */ function wcs_get_order_item( $item_id, $order ) { @@ -818,3 +821,48 @@ function wcs_copy_order_item( $from_item, &$to_item ) { break; } } + +/** + * Checks an order to see if it contains a manual subscription. + * + * @since 2.4.3 + * @param WC_Order|int $order The WC_Order object or ID to get related subscriptions from. + * @param string|array $order_type The order relationship type(s). Can be single string or an array of order types. Optional. Default is 'any'. + * @return bool + */ +function wcs_order_contains_manual_subscription( $order, $order_type = 'any' ) { + $subscriptions = wcs_get_subscriptions_for_order( $order, array( 'order_type' => $order_type ) ); + $contains_manual_subscription = false; + + foreach ( $subscriptions as $subscription ) { + if ( $subscription->is_manual() ) { + $contains_manual_subscription = true; + break; + } + } + + return $contains_manual_subscription; +} + +/** + * Copy payment method from a subscription to an order. + * + * @since 2.4.3 + * @param WC_Subscription $subscription + * @param WC_Order $order + */ +function wcs_copy_payment_method_to_order( $subscription, $order ) { + // Set the order's payment method to match the subscription. + if ( $order->get_payment_method() !== $subscription->get_payment_method() ) { + $order->set_payment_method( $subscription->get_payment_method() ); + } + + // We only need to copy the subscription's post meta to the order. All other payment related meta should already exist. + // Both post_meta and postmeta keys are supported by wcs_set_payment_meta(). + $payment_meta = array_intersect_key( $subscription->get_payment_method_meta(), array_flip( array( 'post_meta', 'postmeta' ) ) ); + $payment_meta = (array) apply_filters( 'wcs_copy_payment_meta_to_order', $payment_meta, $order, $subscription ); + + if ( ! empty( $payment_meta ) ) { + wcs_set_payment_meta( $order, $payment_meta ); + } +} diff --git a/includes/wcs-product-functions.php b/includes/wcs-product-functions.php old mode 100755 new mode 100644 index 7e12038..b132128 --- a/includes/wcs-product-functions.php +++ b/includes/wcs-product-functions.php @@ -169,7 +169,7 @@ function wcs_calculate_min_max_variations( $variations_data ) { $is_max = $is_min = false; - if ( '' === $variation_data['price'] && '' === $variation_data['subscription']['sign_up_fee'] ) { + if ( '' === $variation_data['price'] && empty( $variation_data['subscription']['sign_up_fee'] ) ) { continue; } @@ -194,7 +194,7 @@ function wcs_calculate_min_max_variations( $variations_data ) { $initial_period = $variation_data['subscription']['trial_period']; $initial_interval = $variation_data['subscription']['trial_length']; } else { - $initial_amount = $variation_data['price'] + $variation_data['subscription']['sign_up_fee']; + $initial_amount = (float) $variation_data['price'] + (float) $variation_data['subscription']['sign_up_fee']; $initial_period = $variation_data['subscription']['period']; $initial_interval = $variation_data['subscription']['interval']; } diff --git a/includes/wcs-renewal-functions.php b/includes/wcs-renewal-functions.php old mode 100755 new mode 100644 index 4692926..8c0ea0c --- a/includes/wcs-renewal-functions.php +++ b/includes/wcs-renewal-functions.php @@ -87,8 +87,7 @@ function wcs_cart_contains_renewal() { /** * Checks the cart to see if it contains a subscription product renewal for a failed renewal payment. * - * @param bool | Array The cart item containing the renewal, else false. - * @return string + * @return bool|array The cart item containing the renewal, else false. * @since 2.0 */ function wcs_cart_contains_failed_renewal_order_payment() { diff --git a/includes/wcs-resubscribe-functions.php b/includes/wcs-resubscribe-functions.php old mode 100755 new mode 100644 diff --git a/includes/wcs-switch-functions.php b/includes/wcs-switch-functions.php old mode 100755 new mode 100644 diff --git a/includes/wcs-time-functions.php b/includes/wcs-time-functions.php old mode 100755 new mode 100644 index 883d45f..fb6e42b --- a/includes/wcs-time-functions.php +++ b/includes/wcs-time-functions.php @@ -705,63 +705,76 @@ function wcs_get_days_in_cycle( $period, $interval ) { } /** - * Get an instance of the site's timezone. + * Set a DateTime's timezone to the WordPress site's timezone, or a UTC offset + * if no timezone string is available. * - * @return DateTimeZone Timezone object for the timezone the site is using. + * @since 2.4.2 + * @param WC_DateTime $date + * @return WC_DateTime */ -function wcs_get_sites_timezone() { - - if ( class_exists( 'ActionScheduler_TimezoneHelper' ) ) { - - // Use Action Scheduler's version when possible as it caches the data - $local_timezone = ActionScheduler_TimezoneHelper::get_local_timezone(); +function wcs_set_local_timezone( WC_DateTime $date ) { + if ( get_option( 'timezone_string' ) ) { + $date->setTimezone( new DateTimeZone( wc_timezone_string() ) ); } else { - - $tzstring = get_option( 'timezone_string' ); - - if ( empty( $tzstring ) ) { - - $gmt_offset = get_option( 'gmt_offset' ); - - if ( 0 == $gmt_offset ) { - - $tzstring = 'UTC'; - - } else { - - $gmt_offset *= HOUR_IN_SECONDS; - $tzstring = timezone_name_from_abbr( '', $gmt_offset ); - - if ( false === $tzstring ) { - - $is_dst = date( 'I' ); - - foreach ( timezone_abbreviations_list() as $abbr ) { - - foreach ( $abbr as $city ) { - if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) { - $tzstring = $city['timezone_id']; - break 2; - } - } - } - } - - if ( false === $tzstring ) { - $tzstring = 'UTC'; - } - } - } - - $local_timezone = new DateTimeZone( $tzstring ); + $date->set_utc_offset( wc_timezone_offset() ); } - return $local_timezone; + return $date; } /* Deprecated Functions */ +/** + * Get an instance of the site's timezone. + * + * @return DateTimeZone Timezone object for the timezone the site is using. + * @deprecated 2.4.2 + */ +function wcs_get_sites_timezone() { + _deprecated_function( __FUNCTION__, '2.4.2' ); + + $tzstring = get_option( 'timezone_string' ); + + if ( empty( $tzstring ) ) { + + $gmt_offset = get_option( 'gmt_offset' ); + + if ( 0 == $gmt_offset ) { + + $tzstring = 'UTC'; + + } else { + + $gmt_offset *= HOUR_IN_SECONDS; + $tzstring = timezone_name_from_abbr( '', $gmt_offset ); + + if ( false === $tzstring ) { + + $is_dst = date( 'I' ); + + foreach ( timezone_abbreviations_list() as $abbr ) { + + foreach ( $abbr as $city ) { + if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) { + $tzstring = $city['timezone_id']; + break 2; + } + } + } + } + + if ( false === $tzstring ) { + $tzstring = 'UTC'; + } + } + } + + $local_timezone = new DateTimeZone( $tzstring ); + + return $local_timezone; +} + /** * Returns an array of subscription lengths. * diff --git a/includes/wcs-user-functions.php b/includes/wcs-user-functions.php old mode 100755 new mode 100644 diff --git a/languages/woocommerce-subscriptions.pot b/languages/woocommerce-subscriptions.pot old mode 100755 new mode 100644 index 0f25b84..1de3422 --- a/languages/woocommerce-subscriptions.pot +++ b/languages/woocommerce-subscriptions.pot @@ -2,10 +2,10 @@ # This file is distributed under the same license as the WooCommerce Subscriptions package. msgid "" msgstr "" -"Project-Id-Version: WooCommerce Subscriptions 2.3.7\n" +"Project-Id-Version: WooCommerce Subscriptions 2.4.5\n" "Report-Msgid-Bugs-To: " "https://github.com/Prospress/woocommerce-subscriptions/issues\n" -"POT-Creation-Date: 2018-09-25 00:57:34+00:00\n" +"POT-Creation-Date: 2018-11-28 06:13:23+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -19,74 +19,74 @@ msgstr "" msgid "Invalid relation type: %s. Order relationship type must be one of: %s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:181 +#: includes/admin/class-wc-subscriptions-admin.php:185 msgid "Simple subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:182 +#: includes/admin/class-wc-subscriptions-admin.php:186 msgid "Variable subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:200 +#: includes/admin/class-wc-subscriptions-admin.php:204 msgid "Choose the subscription price, billing interval and period." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:213 +#: includes/admin/class-wc-subscriptions-admin.php:217 #: templates/admin/html-variation-price.php:44 #. translators: placeholder is a currency symbol / code msgid "Subscription price (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:216 +#: includes/admin/class-wc-subscriptions-admin.php:220 msgid "Subscription interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:222 -#: includes/admin/class-wc-subscriptions-admin.php:358 +#: includes/admin/class-wc-subscriptions-admin.php:226 +#: includes/admin/class-wc-subscriptions-admin.php:362 msgid "Subscription period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:236 -#: includes/admin/class-wc-subscriptions-admin.php:359 +#: includes/admin/class-wc-subscriptions-admin.php:240 +#: includes/admin/class-wc-subscriptions-admin.php:363 #: templates/admin/html-variation-price.php:66 msgid "Expire after" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:239 +#: includes/admin/class-wc-subscriptions-admin.php:243 msgid "" "Automatically expire the subscription after this length of time. This " "length is in addition to any free trial or amount of time provided before a " "synchronised first renewal date." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:248 +#: includes/admin/class-wc-subscriptions-admin.php:252 #: templates/admin/html-variation-price.php:20 #. translators: %s is a currency symbol / code msgid "Sign-up fee (%s)" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:250 +#: includes/admin/class-wc-subscriptions-admin.php:254 msgid "" "Optionally include an amount to be charged at the outset of the " "subscription. The sign-up fee will be charged immediately, even if the " "product has a free trial or the payment dates are synced." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:262 +#: includes/admin/class-wc-subscriptions-admin.php:266 #: templates/admin/html-variation-price.php:25 msgid "Free trial" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:265 +#: includes/admin/class-wc-subscriptions-admin.php:269 #: templates/admin/deprecated/html-variation-price.php:115 msgid "Subscription Trial Period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:297 +#: includes/admin/class-wc-subscriptions-admin.php:301 msgid "One time shipping" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:298 +#: includes/admin/class-wc-subscriptions-admin.php:302 msgid "" "Shipping for subscription products is normally charged on the initial order " "and all renewal orders. Enable this to only charge shipping once on the " @@ -94,63 +94,63 @@ msgid "" "not have a free trial or a synced renewal date." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:355 +#: includes/admin/class-wc-subscriptions-admin.php:359 msgid "Subscription pricing" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:356 +#: includes/admin/class-wc-subscriptions-admin.php:360 msgid "Subscription sign-up fee" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:357 +#: includes/admin/class-wc-subscriptions-admin.php:361 msgid "Subscription billing interval" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:360 +#: includes/admin/class-wc-subscriptions-admin.php:364 msgid "Free trial length" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:361 +#: includes/admin/class-wc-subscriptions-admin.php:365 msgid "Free trial period" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:682 +#: includes/admin/class-wc-subscriptions-admin.php:686 msgid "" "Unable to change subscription status to \"%s\". Please assign a customer to " "the subscription to activate it." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:724 +#: includes/admin/class-wc-subscriptions-admin.php:728 msgid "" "Trashing this order will also trash the subscriptions purchased with the " "order." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:737 +#: includes/admin/class-wc-subscriptions-admin.php:741 msgid "Enter the new period, either day, week, month or year:" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:738 +#: includes/admin/class-wc-subscriptions-admin.php:742 msgid "Enter a new length (e.g. 5):" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:739 +#: includes/admin/class-wc-subscriptions-admin.php:743 msgid "" "Enter a new interval as a single number (e.g. to charge every 2nd month, " "enter 2):" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:740 +#: includes/admin/class-wc-subscriptions-admin.php:744 msgid "Delete all variations without a subscription" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:743 +#: includes/admin/class-wc-subscriptions-admin.php:747 msgid "" "Product type can not be changed because this product is associated with " "active subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:747 +#: includes/admin/class-wc-subscriptions-admin.php:751 msgid "" "You are about to trash one or more orders which contain a subscription.\n" "\n" @@ -158,7 +158,7 @@ msgid "" "orders." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:760 +#: includes/admin/class-wc-subscriptions-admin.php:764 msgid "" "WARNING: Bad things are about to happen!\n" "\n" @@ -170,13 +170,13 @@ msgid "" "gateway." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:761 +#: includes/admin/class-wc-subscriptions-admin.php:765 msgid "" "You are deleting a subscription item. You will also need to manually cancel " "and trash the subscription on the Manage Subscriptions screen." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:768 +#: includes/admin/class-wc-subscriptions-admin.php:772 msgid "" "Warning: Deleting a user will also delete the user's subscriptions. The " "user's orders will remain but be reassigned to the 'Guest' user.\n" @@ -185,63 +185,63 @@ msgid "" "subscriptions?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:831 +#: includes/admin/class-wc-subscriptions-admin.php:835 msgid "Active subscriber?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:874 +#: includes/admin/class-wc-subscriptions-admin.php:878 msgid "Manage Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:878 -#: woocommerce-subscriptions.php:314 +#: includes/admin/class-wc-subscriptions-admin.php:882 +#: woocommerce-subscriptions.php:262 msgid "Search Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:898 -#: includes/admin/class-wc-subscriptions-admin.php:1018 -#: includes/admin/class-wcs-admin-reports.php:51 -#: includes/admin/class-wcs-admin-system-status.php:55 -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:687 +#: includes/admin/class-wc-subscriptions-admin.php:902 +#: includes/admin/class-wc-subscriptions-admin.php:1017 +#: includes/admin/class-wcs-admin-reports.php:46 +#: includes/admin/class-wcs-admin-system-status.php:56 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:688 #: includes/class-wcs-query.php:108 includes/class-wcs-query.php:129 #: includes/class-wcs-query.php:248 #: includes/privacy/class-wcs-privacy-exporters.php:51 -#: woocommerce-subscriptions.php:305 woocommerce-subscriptions.php:318 +#: woocommerce-subscriptions.php:253 woocommerce-subscriptions.php:266 msgid "Subscriptions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1058 +#: includes/admin/class-wc-subscriptions-admin.php:1057 msgid "Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1065 +#: includes/admin/class-wc-subscriptions-admin.php:1064 msgid "Add to Cart Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1066 +#: includes/admin/class-wc-subscriptions-admin.php:1065 msgid "" "A product displays a button with the text \"Add to Cart\". By default, a " "subscription changes this to \"Sign Up Now\". You can customise the button " "text for subscriptions here." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1070 -#: includes/admin/class-wc-subscriptions-admin.php:1073 -#: includes/admin/class-wc-subscriptions-admin.php:1082 -#: includes/admin/class-wc-subscriptions-admin.php:1085 +#: includes/admin/class-wc-subscriptions-admin.php:1069 +#: includes/admin/class-wc-subscriptions-admin.php:1072 +#: includes/admin/class-wc-subscriptions-admin.php:1081 +#: includes/admin/class-wc-subscriptions-admin.php:1084 #: includes/class-wc-product-subscription-variation.php:98 #: includes/class-wc-product-subscription.php:72 #: includes/class-wc-product-variable-subscription.php:73 #: includes/class-wc-subscriptions-product.php:99 -#: woocommerce-subscriptions.php:610 +#: woocommerce-subscriptions.php:560 msgid "Sign Up Now" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1077 +#: includes/admin/class-wc-subscriptions-admin.php:1076 msgid "Place Order Button Text" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1078 +#: includes/admin/class-wc-subscriptions-admin.php:1077 msgid "" "Use this field to customise the text displayed on the checkout button when " "an order contains a subscription. Normally the checkout submission button " @@ -249,11 +249,11 @@ msgid "" "changed to \"Sign Up Now\"." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1091 +#: includes/admin/class-wc-subscriptions-admin.php:1090 msgid "Roles" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1094 +#: includes/admin/class-wc-subscriptions-admin.php:1093 #. translators: placeholders are tags msgid "" "Choose the default roles to assign to active and inactive subscribers. For " @@ -262,46 +262,46 @@ msgid "" "allocated these roles to prevent locking out administrators." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1099 +#: includes/admin/class-wc-subscriptions-admin.php:1098 msgid "Subscriber Default Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1100 +#: includes/admin/class-wc-subscriptions-admin.php:1099 msgid "" "When a subscription is activated, either manually or after a successful " "purchase, new users will be assigned this role." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1111 +#: includes/admin/class-wc-subscriptions-admin.php:1110 msgid "Inactive Subscriber Role" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1112 +#: includes/admin/class-wc-subscriptions-admin.php:1111 msgid "" "If a subscriber's subscription is manually cancelled or expires, she will " "be assigned this role." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1132 +#: includes/admin/class-wc-subscriptions-admin.php:1131 msgid "Manual Renewal Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1133 +#: includes/admin/class-wc-subscriptions-admin.php:1132 msgid "Accept Manual Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1138 +#: includes/admin/class-wc-subscriptions-admin.php:1137 #. translators: placeholders are opening and closing link tags msgid "" "With manual renewals, a customer's subscription is put on-hold until they " "login and pay to renew it. %sLearn more%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1144 +#: includes/admin/class-wc-subscriptions-admin.php:1143 msgid "Turn off Automatic Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1149 +#: includes/admin/class-wc-subscriptions-admin.php:1148 #. translators: placeholders are opening and closing link tags msgid "" "If you don't want new subscription purchases to automatically charge " @@ -310,11 +310,11 @@ msgid "" "more%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1164 +#: includes/admin/class-wc-subscriptions-admin.php:1163 msgid "Customer Suspensions" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1171 +#: includes/admin/class-wc-subscriptions-admin.php:1170 msgid "" "Set a maximum number of times a customer can suspend their account for each " "billing period. For example, for a value of 3 and a subscription billed " @@ -324,30 +324,30 @@ msgid "" "this to 0 to turn off the customer suspension feature completely." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1175 +#: includes/admin/class-wc-subscriptions-admin.php:1174 msgid "Mixed Checkout" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1176 +#: includes/admin/class-wc-subscriptions-admin.php:1175 msgid "Allow multiple subscriptions and products to be purchased simultaneously." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1180 +#: includes/admin/class-wc-subscriptions-admin.php:1179 msgid "" "Allow a subscription product to be purchased with other products and " "subscriptions in the same transaction." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1184 +#: includes/admin/class-wc-subscriptions-admin.php:1183 #: includes/upgrades/templates/wcs-about-2-0.php:108 msgid "Drip Downloadable Content" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1185 +#: includes/admin/class-wc-subscriptions-admin.php:1184 msgid "Enable dripping for downloadable content on subscription products." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1189 +#: includes/admin/class-wc-subscriptions-admin.php:1188 msgid "" "Enabling this grants access to new downloadable files added to a product " "only after the next renewal is processed.%sBy default, access to new " @@ -355,7 +355,7 @@ msgid "" "customer that has an active subscription with that product." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1225 +#: includes/admin/class-wc-subscriptions-admin.php:1224 #. translators: $1-$2: opening and closing tags, $3-$4: opening and #. closing tags msgid "" @@ -363,73 +363,73 @@ msgid "" "start selling subscriptions!%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1230 +#: includes/admin/class-wc-subscriptions-admin.php:1229 msgid "Add a Subscription Product" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1231 +#: includes/admin/class-wc-subscriptions-admin.php:1230 #: includes/upgrades/templates/wcs-about-2-0.php:35 #: includes/upgrades/templates/wcs-about.php:34 -#: woocommerce-subscriptions.php:1196 +#: woocommerce-subscriptions.php:1091 msgid "Settings" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1317 +#: includes/admin/class-wc-subscriptions-admin.php:1316 #. translators: placeholder is a number msgid "We can't find a subscription with ID #%d. Perhaps it was deleted?" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1350 -#: includes/admin/class-wc-subscriptions-admin.php:1355 +#: includes/admin/class-wc-subscriptions-admin.php:1349 +#: includes/admin/class-wc-subscriptions-admin.php:1354 #. translators: placeholders are opening link tag, ID of sub, and closing link #. tag msgid "Showing orders for %sSubscription %s%s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1379 +#: includes/admin/class-wc-subscriptions-admin.php:1378 #. translators: number of 1$: days, 2$: weeks, 3$: months, 4$: years msgid "The trial period can not exceed: %1s, %2s, %3s or %4s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1384 +#: includes/admin/class-wc-subscriptions-admin.php:1383 #. translators: placeholder is a time period (e.g. "4 weeks") msgid "The trial period can not exceed %s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1447 -#: includes/admin/class-wc-subscriptions-admin.php:1500 -#: includes/admin/class-wcs-admin-system-status.php:94 -#: includes/admin/reports/class-wcs-report-cache-manager.php:341 +#: includes/admin/class-wc-subscriptions-admin.php:1446 +#: includes/admin/class-wc-subscriptions-admin.php:1499 +#: includes/admin/class-wcs-admin-system-status.php:95 +#: includes/admin/reports/class-wcs-report-cache-manager.php:328 msgid "Yes" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1447 -#: includes/admin/class-wcs-admin-system-status.php:94 -#: includes/admin/reports/class-wcs-report-cache-manager.php:341 +#: includes/admin/class-wc-subscriptions-admin.php:1446 +#: includes/admin/class-wcs-admin-system-status.php:95 +#: includes/admin/reports/class-wcs-report-cache-manager.php:328 msgid "No" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1483 +#: includes/admin/class-wc-subscriptions-admin.php:1482 msgid "Automatic Recurring Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1500 +#: includes/admin/class-wc-subscriptions-admin.php:1499 msgid "" "Supports automatic renewal payments with the WooCommerce Subscriptions " "extension." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1580 +#: includes/admin/class-wc-subscriptions-admin.php:1579 msgid "Subscription items can no longer be edited." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1584 +#: includes/admin/class-wc-subscriptions-admin.php:1583 msgid "" "This subscription is no longer editable because the payment gateway does " "not allow modification of recurring amounts." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1603 +#: includes/admin/class-wc-subscriptions-admin.php:1602 #. translators: $1-2: opening and closing tags of a link that takes to Woo #. marketplace / Stripe product page msgid "" @@ -438,18 +438,18 @@ msgid "" "the %1$sfree Stripe extension%2$s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1608 +#: includes/admin/class-wc-subscriptions-admin.php:1607 msgid "Recurring Payments" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1616 +#: includes/admin/class-wc-subscriptions-admin.php:1615 #. translators: placeholders are opening and closing link tags msgid "" "Payment gateways which don't support automatic recurring payments can be " "used to process %smanual subscription renewal payments%s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1623 +#: includes/admin/class-wc-subscriptions-admin.php:1622 #. translators: $1-$2: opening and closing tags. Link to documents->payment #. gateways, 3$-4$: opening and closing tags. Link to WooCommerce extensions #. shop page @@ -458,7 +458,7 @@ msgid "" "the official %3$sWooCommerce Marketplace%4$s." msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1725 +#: includes/admin/class-wc-subscriptions-admin.php:1724 msgid "Note that purchasing a subscription still requires an account." msgstr "" @@ -576,7 +576,7 @@ msgstr "" #: templates/emails/subscription-info.php:18 #: templates/myaccount/my-subscriptions.php:25 #: templates/myaccount/related-subscriptions.php:20 -#: woocommerce-subscriptions.php:306 +#: woocommerce-subscriptions.php:254 msgid "Subscription" msgstr "" @@ -687,117 +687,117 @@ msgid "" "this subscription controls when payments are processed." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:873 -#: includes/admin/class-wcs-admin-post-types.php:876 -#: includes/admin/class-wcs-admin-post-types.php:879 +#: includes/admin/class-wcs-admin-post-types.php:884 +#: includes/admin/class-wcs-admin-post-types.php:887 +#: includes/admin/class-wcs-admin-post-types.php:890 msgid "Subscription updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:874 +#: includes/admin/class-wcs-admin-post-types.php:885 msgid "Custom field updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:875 +#: includes/admin/class-wcs-admin-post-types.php:886 msgid "Custom field deleted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:880 +#: includes/admin/class-wcs-admin-post-types.php:891 msgid "Subscription saved." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:881 +#: includes/admin/class-wcs-admin-post-types.php:892 msgid "Subscription submitted." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:883 +#: includes/admin/class-wcs-admin-post-types.php:894 #. translators: php date string msgid "Subscription scheduled for: %1$s." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:884 +#: includes/admin/class-wcs-admin-post-types.php:895 msgid "Subscription draft updated." msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:920 +#: includes/admin/class-wcs-admin-post-types.php:931 msgid "Any Payment Method" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:921 +#: includes/admin/class-wcs-admin-post-types.php:932 msgid "None" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:1114 +#: includes/admin/class-wcs-admin-post-types.php:1125 #. translators: 1: user display name 2: user ID 3: user email msgid "%1$s (#%2$s – %3$s)" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:1121 +#: includes/admin/class-wcs-admin-post-types.php:1132 #: includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php:84 msgid "Search for a customer…" msgstr "" -#: includes/admin/class-wcs-admin-reports.php:54 +#: includes/admin/class-wcs-admin-reports.php:49 msgid "Subscription Events by Date" msgstr "" -#: includes/admin/class-wcs-admin-reports.php:60 +#: includes/admin/class-wcs-admin-reports.php:55 msgid "Upcoming Recurring Revenue" msgstr "" -#: includes/admin/class-wcs-admin-reports.php:66 +#: includes/admin/class-wcs-admin-reports.php:61 msgid "Retention Rate" msgstr "" -#: includes/admin/class-wcs-admin-reports.php:72 +#: includes/admin/class-wcs-admin-reports.php:67 msgid "Subscriptions by Product" msgstr "" -#: includes/admin/class-wcs-admin-reports.php:78 +#: includes/admin/class-wcs-admin-reports.php:73 msgid "Subscriptions by Customer" msgstr "" -#: includes/admin/class-wcs-admin-reports.php:88 +#: includes/admin/class-wcs-admin-reports.php:83 msgid "Failed Payment Retries" msgstr "" -#: includes/admin/class-wcs-admin-reports.php:128 -#: includes/admin/reports/class-wcs-report-cache-manager.php:287 +#: includes/admin/class-wcs-admin-reports.php:104 +#: includes/admin/reports/class-wcs-report-cache-manager.php:274 msgid "WooCommerce" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:56 +#: includes/admin/class-wcs-admin-system-status.php:57 msgid "This section shows any information about Subscriptions." msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:60 +#: includes/admin/class-wcs-admin-system-status.php:61 msgid "Store Setup" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:61 +#: includes/admin/class-wcs-admin-system-status.php:62 msgid "This section shows general information about the store." msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:65 +#: includes/admin/class-wcs-admin-system-status.php:66 msgid "Subscriptions by Payment Gateway" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:66 +#: includes/admin/class-wcs-admin-system-status.php:67 msgid "This section shows information about Subscription payment methods." msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:70 +#: includes/admin/class-wcs-admin-system-status.php:71 msgid "Payment Gateway Support" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:71 +#: includes/admin/class-wcs-admin-system-status.php:72 msgid "This section shows information about payment gateway feature support." msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:128 +#: includes/admin/class-wcs-admin-system-status.php:144 msgid "%sLearn how to update%s" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:174 +#: includes/admin/class-wcs-admin-system-status.php:190 #. translators: %1$s is the file version, %2$s is the core version msgid "version %1$s is out of date. The core version is %2$s" msgstr "" @@ -911,9 +911,9 @@ msgid "Relationship" msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-table.php:19 -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:548 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:549 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:173 -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:205 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:204 #: templates/myaccount/related-orders.php:23 #: templates/myaccount/related-orders.php:42 msgid "Date" @@ -986,18 +986,18 @@ msgstr "" msgid "Error: unable to find timezone of your browser." msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:290 +#: includes/admin/reports/class-wcs-report-cache-manager.php:277 msgid "" "Please note: data for this report is cached. The data displayed may be out " "of date by up to 24 hours. The cache is updated each morning at 4am in your " "site's timezone." msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:345 +#: includes/admin/reports/class-wcs-report-cache-manager.php:332 msgid "Cache Update Failures" msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:348 +#: includes/admin/reports/class-wcs-report-cache-manager.php:335 #. translators: %d refers to the number of times we have detected cache update #. failures msgid "%d failures" @@ -1143,185 +1143,185 @@ msgstr "" msgid "subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:397 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:398 msgid "%s signup revenue in this period" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:398 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:399 msgid "" "The sum of all subscription parent orders, including other items, fees, tax " "and shipping." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:404 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:405 msgid "%s renewal revenue in this period" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:405 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:406 msgid "The sum of all renewal orders including tax and shipping." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:411 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:412 msgid "%s resubscribe revenue in this period" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:412 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:413 msgid "The sum of all resubscribe orders including tax and shipping." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:418 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:419 msgid "%s new subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:419 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:420 msgid "" "The number of subscriptions created during this period, either by being " "manually created, imported or a customer placing an order." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:425 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:426 msgid "%s subscription signups" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:426 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:427 msgid "" "The number of subscription parent orders created during this period. This " "represents the new subscriptions created by customers placing an order via " "checkout." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:432 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:433 msgid "%s subscription resubscribes" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:433 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:434 msgid "The number of resubscribe orders processed during this period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:439 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:440 msgid "%s subscription renewals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:440 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:441 msgid "The number of renewal orders processed during this period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:446 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:447 msgid "%s subscription switches" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:447 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:448 msgid "" "The number of subscriptions upgraded, downgraded or cross-graded during " "this period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:453 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:454 msgid "%s subscription cancellations" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:454 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:455 msgid "" "The number of subscriptions cancelled by the customer or store manager " "during this period. The pre-paid term may not yet have ended during this " "period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:460 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:461 msgid "%s subscriptions ended" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:461 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:462 msgid "" "The number of subscriptions which have either expired or reached the end of " "the prepaid term if it was previously cancelled." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:467 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:468 msgid "%s current subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:468 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:469 msgid "" "The number of subscriptions during this period with an end date in the " "future and a status other than pending." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:484 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:485 msgid "%s net subscription gain" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:486 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:487 msgid "%s net subscription loss" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:491 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:492 msgid "Change in subscriptions between the start and end of the period." msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:505 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:506 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:137 msgid "Year" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:506 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:507 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:138 msgid "Last Month" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:507 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:508 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:139 msgid "This Month" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:508 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:509 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:140 msgid "Last 7 Days" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:552 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:553 #: includes/admin/reports/class-wcs-report-subscription-payment-retry.php:177 -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:209 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:208 msgid "Export CSV" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:609 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:610 msgid "Switched subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:625 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:626 msgid "New Subscriptions" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:641 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:642 msgid "Subscriptions signups" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:656 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:657 msgid "Number of resubscribes" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:671 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:672 msgid "Number of renewals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:703 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:704 msgid "Subscriptions Ended" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:719 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:720 msgid "Cancellations" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:734 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:735 msgid "Signup Totals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:754 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:755 msgid "Resubscribe Totals" msgstr "" -#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:774 +#: includes/admin/reports/class-wcs-report-subscription-events-by-date.php:775 msgid "Renewal Totals" msgstr "" @@ -1398,40 +1398,40 @@ msgstr "" msgid "%s average renewal amount" msgstr "" -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:175 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:174 msgid "Next 12 Months" msgstr "" -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:176 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:175 msgid "Next 30 Days" msgstr "" -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:177 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:176 msgid "Next Month" msgstr "" -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:178 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:177 msgid "Next 7 Days" msgstr "" -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:243 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:242 msgid "Renewals count" msgstr "" -#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:252 +#: includes/admin/reports/class-wcs-report-upcoming-recurring-revenue.php:251 msgid "Renewals amount" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:118 +#: includes/api/class-wc-rest-subscriptions-controller.php:116 msgid "Customer ID is invalid." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:223 +#: includes/api/class-wc-rest-subscriptions-controller.php:221 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:171 msgid "Invalid subscription id." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:294 +#: includes/api/class-wc-rest-subscriptions-controller.php:292 #: includes/api/legacy/class-wc-api-subscriptions.php:307 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:303 msgid "" @@ -1439,7 +1439,7 @@ msgid "" "Subscription." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:325 +#: includes/api/class-wc-rest-subscriptions-controller.php:330 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:333 #. translators: 1$: gateway id, 2$: error message msgid "" @@ -1447,71 +1447,71 @@ msgid "" "%2$s" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:392 -#: includes/api/class-wc-rest-subscriptions-controller.php:558 +#: includes/api/class-wc-rest-subscriptions-controller.php:396 +#: includes/api/class-wc-rest-subscriptions-controller.php:562 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:284 msgid "Updating subscription dates errored with message: %s" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:417 +#: includes/api/class-wc-rest-subscriptions-controller.php:421 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:347 msgid "The number of billing periods between subscription renewals." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:422 +#: includes/api/class-wc-rest-subscriptions-controller.php:426 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:352 msgid "Billing period for the subscription." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:428 +#: includes/api/class-wc-rest-subscriptions-controller.php:432 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:358 msgid "Subscription payment details." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:433 +#: includes/api/class-wc-rest-subscriptions-controller.php:437 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:363 msgid "Payment gateway ID." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:440 +#: includes/api/class-wc-rest-subscriptions-controller.php:444 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:370 msgid "The subscription's start date." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:445 +#: includes/api/class-wc-rest-subscriptions-controller.php:449 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:375 msgid "The subscription's trial date" msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:450 +#: includes/api/class-wc-rest-subscriptions-controller.php:454 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:380 msgid "The subscription's next payment date." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:455 +#: includes/api/class-wc-rest-subscriptions-controller.php:459 #: includes/api/legacy/class-wc-rest-subscriptions-controller.php:385 msgid "The subscription's end date." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:460 +#: includes/api/class-wc-rest-subscriptions-controller.php:464 msgid "" "The subscription's original subscription ID if this is a resubscribed " "subscription." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:466 +#: includes/api/class-wc-rest-subscriptions-controller.php:470 msgid "The subscription's resubscribed subscription ID." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:472 +#: includes/api/class-wc-rest-subscriptions-controller.php:476 msgid "The date the subscription's latest order was completed, in GMT." msgstr "" -#: includes/api/class-wc-rest-subscriptions-controller.php:478 +#: includes/api/class-wc-rest-subscriptions-controller.php:482 msgid "The date the subscription's latest order was paid, in GMT." msgstr "" -#: includes/api/legacy/class-wc-api-subscriptions.php:102 wcs-functions.php:172 +#: includes/api/legacy/class-wc-api-subscriptions.php:102 wcs-functions.php:178 msgid "Invalid subscription status given." msgstr "" @@ -1534,13 +1534,13 @@ msgid "" "manual with error message: %2$s" msgstr "" -#: includes/api/legacy/class-wc-api-subscriptions.php:380 wcs-functions.php:146 +#: includes/api/legacy/class-wc-api-subscriptions.php:380 wcs-functions.php:152 msgid "" "Invalid subscription billing interval given. Must be an integer greater " "than 0." msgstr "" -#: includes/api/legacy/class-wc-api-subscriptions.php:391 wcs-functions.php:141 +#: includes/api/legacy/class-wc-api-subscriptions.php:391 wcs-functions.php:147 msgid "Invalid subscription billing period given." msgstr "" @@ -1548,71 +1548,75 @@ msgstr "" msgid "Cannot create subscription: %s." msgstr "" -#: includes/class-wc-subscription.php:398 +#: includes/class-wc-subscription.php:413 msgid "Unable to change subscription status to \"%s\"." msgstr "" -#: includes/class-wc-subscription.php:500 +#: includes/class-wc-subscription.php:515 msgid "Unable to change subscription status to \"%s\". Exception: %s" msgstr "" -#: includes/class-wc-subscription.php:522 +#: includes/class-wc-subscription.php:537 #. translators: 1: old subscription status 2: new subscription status msgid "Status changed from %1$s to %2$s." msgstr "" -#: includes/class-wc-subscription.php:534 +#: includes/class-wc-subscription.php:549 #. translators: %s: new order status msgid "Status set to %s." msgstr "" -#: includes/class-wc-subscription.php:1085 +#: includes/class-wc-subscription.php:1100 #: includes/class-wc-subscriptions-manager.php:2279 #: includes/wcs-formatting-functions.php:228 #. translators: placeholder is human time diff (e.g. "3 weeks") msgid "In %s" msgstr "" -#: includes/class-wc-subscription.php:1088 +#: includes/class-wc-subscription.php:1103 #: includes/wcs-formatting-functions.php:231 #. translators: placeholder is human time diff (e.g. "3 weeks") msgid "%s ago" msgstr "" -#: includes/class-wc-subscription.php:1095 +#: includes/class-wc-subscription.php:1110 msgid "Not yet ended" msgstr "" -#: includes/class-wc-subscription.php:1098 +#: includes/class-wc-subscription.php:1113 msgid "Not cancelled" msgstr "" -#: includes/class-wc-subscription.php:1213 +#: includes/class-wc-subscription.php:1228 +msgid "The creation date of a subscription can not be deleted, only updated." +msgstr "" + +#: includes/class-wc-subscription.php:1231 msgid "The start date of a subscription can not be deleted, only updated." msgstr "" -#: includes/class-wc-subscription.php:1217 +#: includes/class-wc-subscription.php:1235 msgid "The %s date of a subscription can not be deleted. You must delete the order." msgstr "" -#: includes/class-wc-subscription.php:1225 -#: includes/class-wc-subscription.php:2301 +#: includes/class-wc-subscription.php:1243 +#: includes/class-wc-subscription.php:2302 msgid "Subscription #%d: " msgstr "" -#: includes/class-wc-subscription.php:1631 +#: includes/class-wc-subscription.php:1650 msgid "Payment status marked complete." msgstr "" -#: includes/class-wc-subscription.php:1659 +#: includes/class-wc-subscription.php:1678 msgid "Payment failed." msgstr "" -#: includes/class-wc-subscription.php:1664 +#: includes/class-wc-subscription.php:1683 msgid "Subscription Cancelled: maximum number of failed payments reached." msgstr "" -#: includes/class-wc-subscription.php:1774 +#: includes/class-wc-subscription.php:1793 msgid "" "The \"all\" value for $order_type parameter is deprecated. It was a " "misnomer, as it did not return resubscribe orders. It was also inconsistent " @@ -1622,155 +1626,155 @@ msgid "" "resubscribe." msgstr "" -#: includes/class-wc-subscription.php:1876 +#: includes/class-wc-subscription.php:1895 #: includes/class-wcs-change-payment-method-admin.php:155 msgid "Manual Renewal" msgstr "" -#: includes/class-wc-subscription.php:1955 +#: includes/class-wc-subscription.php:1974 wcs-functions.php:794 msgid "Payment method meta must be an array." msgstr "" -#: includes/class-wc-subscription.php:2199 +#: includes/class-wc-subscription.php:2200 msgid "Invalid format. First parameter needs to be an array." msgstr "" -#: includes/class-wc-subscription.php:2203 +#: includes/class-wc-subscription.php:2204 msgid "Invalid data. First parameter was empty when passed to update_dates()." msgstr "" -#: includes/class-wc-subscription.php:2210 +#: includes/class-wc-subscription.php:2211 msgid "" "Invalid data. First parameter has a date that is not in the registered date " "types." msgstr "" -#: includes/class-wc-subscription.php:2274 +#: includes/class-wc-subscription.php:2275 msgid "The %s date must occur after the cancellation date." msgstr "" -#: includes/class-wc-subscription.php:2279 +#: includes/class-wc-subscription.php:2280 msgid "The %s date must occur after the last payment date." msgstr "" -#: includes/class-wc-subscription.php:2283 +#: includes/class-wc-subscription.php:2284 msgid "The %s date must occur after the next payment date." msgstr "" -#: includes/class-wc-subscription.php:2288 +#: includes/class-wc-subscription.php:2289 msgid "The %s date must occur after the trial end date." msgstr "" -#: includes/class-wc-subscription.php:2292 +#: includes/class-wc-subscription.php:2293 msgid "The %s date must occur after the start date." msgstr "" -#: includes/class-wc-subscription.php:2321 +#: includes/class-wc-subscription.php:2322 #: includes/class-wc-subscriptions-checkout.php:325 #: includes/wcs-order-functions.php:305 msgid "Backordered" msgstr "" -#: includes/class-wc-subscriptions-addresses.php:46 +#: includes/class-wc-subscriptions-addresses.php:47 msgid "Change Address" msgstr "" -#: includes/class-wc-subscriptions-addresses.php:70 +#: includes/class-wc-subscriptions-addresses.php:71 msgid "" "Both the shipping address used for the subscription and your default " "shipping address for future purchases will be updated." msgstr "" -#: includes/class-wc-subscriptions-addresses.php:83 +#: includes/class-wc-subscriptions-addresses.php:84 #. translators: $1: address type (Shipping Address / Billing Address), $2: #. opening tag, $3: closing tag msgid "Update the %1$s used for %2$sall%3$s of my active subscriptions" msgstr "" -#: includes/class-wc-subscriptions-cart.php:902 +#: includes/class-wc-subscriptions-cart.php:912 msgid "Please enter a valid postcode/ZIP." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1073 +#: includes/class-wc-subscriptions-cart.php:1083 msgid "" "That subscription product can not be added to your cart as it already " "contains a subscription renewal." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1161 +#: includes/class-wc-subscriptions-cart.php:1171 msgid "Invalid recurring shipping method." msgstr "" -#: includes/class-wc-subscriptions-cart.php:1984 +#: includes/class-wc-subscriptions-cart.php:1994 msgid "now" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:160 +#: includes/class-wc-subscriptions-change-payment-gateway.php:175 #: templates/emails/plain/email-order-details.php:19 #. translators: placeholder is the subscription order number wrapped in #. tags msgid "Subscription Number: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:166 +#: includes/class-wc-subscriptions-change-payment-gateway.php:181 #. translators: placeholder is the subscription's next payment date (either #. human readable or normal date) wrapped in tags msgid "Next Payment Date: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:172 +#: includes/class-wc-subscriptions-change-payment-gateway.php:187 #. translators: placeholder is the formatted total to be paid for the #. subscription wrapped in tags msgid "Total: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:179 +#: includes/class-wc-subscriptions-change-payment-gateway.php:194 #. translators: placeholder is the display name of the payment method msgid "Payment Method: %s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:191 +#: includes/class-wc-subscriptions-change-payment-gateway.php:206 msgid "" "Sorry, this subscription change payment method request is invalid and " "cannot be processed." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:223 +#: includes/class-wc-subscriptions-change-payment-gateway.php:238 msgid "There was an error with your request. Please try again." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:227 +#: includes/class-wc-subscriptions-change-payment-gateway.php:242 #: templates/myaccount/view-subscription.php:20 msgid "Invalid Subscription." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:231 +#: includes/class-wc-subscriptions-change-payment-gateway.php:246 #: includes/class-wcs-cart-resubscribe.php:78 #: includes/class-wcs-cart-resubscribe.php:129 #: includes/class-wcs-user-change-status-handler.php:103 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:96 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:94 msgid "That doesn't appear to be one of your subscriptions." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:235 +#: includes/class-wc-subscriptions-change-payment-gateway.php:250 msgid "The payment method can not be changed for that subscription." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:241 +#: includes/class-wc-subscriptions-change-payment-gateway.php:256 #. translators: placeholder is next payment's date msgid " Next payment is due %s." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:247 +#: includes/class-wc-subscriptions-change-payment-gateway.php:262 #. translators: placeholder is either empty or "Next payment is due..." msgid "Choose a new payment method.%s" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:276 +#: includes/class-wc-subscriptions-change-payment-gateway.php:291 msgid "Invalid order." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:378 +#: includes/class-wc-subscriptions-change-payment-gateway.php:393 msgid "Payment method updated." msgstr "" @@ -1845,7 +1849,7 @@ msgid "" msgstr "" #: includes/class-wc-subscriptions-coupon.php:499 -msgid "Sorry, only recurring coupons can only be applied to subscriptions." +msgid "Sorry, only recurring coupons can be applied to subscriptions." msgstr "" #: includes/class-wc-subscriptions-coupon.php:681 @@ -1860,40 +1864,48 @@ msgstr "" msgid "Renewal cart discount" msgstr "" -#: includes/class-wc-subscriptions-coupon.php:700 +#: includes/class-wc-subscriptions-coupon.php:684 +msgid "Initial payment discount" +msgstr "" + +#: includes/class-wc-subscriptions-coupon.php:701 msgid "Renewal Discount" msgstr "" -#: includes/class-wc-subscriptions-coupon.php:915 +#: includes/class-wc-subscriptions-coupon.php:704 +msgid "Discount" +msgstr "" + +#: includes/class-wc-subscriptions-coupon.php:920 msgid "" "Sorry, it seems there are no available payment methods which support the " "recurring coupon you are using. Please contact us if you require assistance " "or wish to make alternate arrangements." msgstr "" -#: includes/class-wc-subscriptions-coupon.php:930 +#: includes/class-wc-subscriptions-coupon.php:935 msgid "Active for x payments" msgstr "" -#: includes/class-wc-subscriptions-coupon.php:931 +#: includes/class-wc-subscriptions-coupon.php:936 msgid "Unlimited payments" msgstr "" -#: includes/class-wc-subscriptions-coupon.php:932 +#: includes/class-wc-subscriptions-coupon.php:937 msgid "" "Coupon will be limited to the given number of payments. It will then be " "automatically removed from the subscription. \"Payments\" also includes the " "initial subscription payment." msgstr "" -#: includes/class-wc-subscriptions-coupon.php:1064 +#: includes/class-wc-subscriptions-coupon.php:1069 #. translators: %d refers to the number of payments the coupon can be used for. msgid "Active for %d payment" msgid_plural "Active for %d payments" msgstr[0] "" msgstr[1] "" -#: includes/class-wc-subscriptions-coupon.php:1068 +#: includes/class-wc-subscriptions-coupon.php:1073 msgid "Active for unlimited payments" msgstr "" @@ -1902,7 +1914,7 @@ msgid "Error: Unable to create renewal order with note \"%s\"" msgstr "" #: includes/class-wc-subscriptions-manager.php:168 -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:210 +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:209 msgid "Subscription doesn't exist in scheduled action: %d" msgstr "" @@ -2094,7 +2106,7 @@ msgstr "" #. translators: 1$: recurring amount, 2$: subscription period (e.g. "month" or #. "3 months") (e.g. "$15 / month" or "$15 every 2nd month") msgid "%1$s / %2$s" -msgid_plural " %1$s every %2$s" +msgid_plural "%1$s every %2$s" msgstr[0] "" msgstr[1] "" @@ -2242,7 +2254,7 @@ msgstr "" #: includes/class-wc-subscriptions-switcher.php:421 #: includes/class-wc-subscriptions-switcher.php:447 -#: includes/class-wc-subscriptions-switcher.php:2446 +#: includes/class-wc-subscriptions-switcher.php:2363 msgid "Upgrade or Downgrade" msgstr "" @@ -2280,19 +2292,20 @@ msgstr "" msgid "There was an error locating the switch details." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1896 -#: includes/class-wc-subscriptions-switcher.php:2240 +#: includes/class-wc-subscriptions-switcher.php:1413 +msgid "" +"Invalid switch type \"%s\". Switch must be one of: \"upgrade\", " +"\"downgrade\" or \"crossgrade\"." +msgstr "" + +#: includes/class-wc-subscriptions-switcher.php:1907 msgid "The original subscription item being switched cannot be found." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1898 +#: includes/class-wc-subscriptions-switcher.php:1909 msgid "The item on the switch order cannot be found." msgstr "" -#: includes/class-wc-subscriptions-switcher.php:2277 -msgid "Failed to update the subscription shipping method." -msgstr "" - #: includes/class-wc-subscriptions-synchroniser.php:48 msgid "Synchronise renewals" msgstr "" @@ -2381,55 +2394,55 @@ msgstr "" msgid "View and manage subscriptions" msgstr "" -#: includes/class-wcs-cached-data-manager.php:76 +#: includes/class-wcs-cached-data-manager.php:79 msgid "Related order caching is now handled by %1$s." msgstr "" -#: includes/class-wcs-cached-data-manager.php:83 +#: includes/class-wcs-cached-data-manager.php:86 msgid "Customer subscription caching is now handled by %1$s." msgstr "" -#: includes/class-wcs-cached-data-manager.php:107 -#: includes/class-wcs-cached-data-manager.php:237 +#: includes/class-wcs-cached-data-manager.php:110 +#: includes/class-wcs-cached-data-manager.php:240 msgid "Customer subscription caching is now handled by %1$s and %2$s." msgstr "" -#: includes/class-wcs-cached-data-manager.php:124 +#: includes/class-wcs-cached-data-manager.php:127 msgid "new related order methods in WCS_Related_Order_Store" msgstr "" -#: includes/class-wcs-cached-data-manager.php:222 +#: includes/class-wcs-cached-data-manager.php:225 msgid "Weekly" msgstr "" #: includes/class-wcs-cart-initial-payment.php:59 -#: includes/class-wcs-cart-renewal.php:182 +#: includes/class-wcs-cart-renewal.php:188 msgid "That doesn't appear to be your order." msgstr "" -#: includes/class-wcs-cart-renewal.php:198 +#: includes/class-wcs-cart-renewal.php:204 msgid "" "This order can no longer be paid because the corresponding subscription " "does not require payment at this time." msgstr "" -#: includes/class-wcs-cart-renewal.php:215 +#: includes/class-wcs-cart-renewal.php:221 msgid "Complete checkout to renew your subscription." msgstr "" -#: includes/class-wcs-cart-renewal.php:296 +#: includes/class-wcs-cart-renewal.php:302 #. translators: placeholder is an item name msgid "" "The %s product has been deleted and can no longer be renewed. Please choose " "a new product or contact us for assistance." msgstr "" -#: includes/class-wcs-cart-renewal.php:330 +#: includes/class-wcs-cart-renewal.php:336 #. translators: %s is subscription's number msgid "Subscription #%s has not been added to the cart." msgstr "" -#: includes/class-wcs-cart-renewal.php:459 +#: includes/class-wcs-cart-renewal.php:371 msgid "" "We couldn't find the original subscription for an item in your cart. The " "item was removed." @@ -2439,7 +2452,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: includes/class-wcs-cart-renewal.php:466 +#: includes/class-wcs-cart-renewal.php:378 msgid "" "We couldn't find the original renewal order for an item in your cart. The " "item was removed." @@ -2449,7 +2462,7 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: includes/class-wcs-cart-renewal.php:741 +#: includes/class-wcs-cart-renewal.php:653 msgid "All linked subscription items have been removed from the cart." msgstr "" @@ -2458,7 +2471,7 @@ msgid "There was an error with your request to resubscribe. Please try again." msgstr "" #: includes/class-wcs-cart-resubscribe.php:74 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:92 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:90 msgid "That subscription does not exist. Has it been deleted?" msgstr "" @@ -2481,33 +2494,14 @@ msgstr "" msgid "Please choose a valid payment gateway to change to." msgstr "" -#: includes/class-wcs-failed-scheduled-action-manager.php:128 -#. translators: $1: Opening previously translated sentence $2,$5,$9 opening -#. link tags $3 closing link tag $4 opening paragraph tag $6 closing paragraph -#. tag $7 list of affected actions wrapped in code tags $8 the log file name -#. $10 div containing a group of buttons/links -msgid "" -"%1$s Please %2$sopen a new ticket at WooCommerce Support%3$s immediately to " -"get this resolved.%4$sTo resolve this error a quickly as possible, please " -"include login details for a %5$stemporary administrator " -"account%3$s.%6$sAffected events: %7$s%4$sTo see further details, view the " -"%8$s log file from the %9$sWooCommerce logs screen.%3$s%6$s%10$s" +#: includes/class-wcs-failed-scheduled-action-manager.php:134 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:178 +msgid "Ignore this error (not recommended)" msgstr "" -#: includes/class-wcs-failed-scheduled-action-manager.php:129 -msgid "An error has occurred while processing a recent subscription related event." -msgid_plural "An error has occurred while processing recent subscription related events." -msgstr[0] "" -msgstr[1] "" - -#: includes/class-wcs-failed-scheduled-action-manager.php:138 -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:190 -msgid "Ignore this error (not recommended!)" -msgstr "" - -#: includes/class-wcs-failed-scheduled-action-manager.php:138 -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:190 -msgid "Open up a ticket now!" +#: includes/class-wcs-failed-scheduled-action-manager.php:139 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:183 +msgid "Open a ticket" msgstr "" #: includes/class-wcs-limiter.php:45 @@ -2616,6 +2610,12 @@ msgid "" "not support removing an item." msgstr "" +#: includes/class-wcs-retry-manager.php:316 +msgid "" +"Payment retry attempted on renewal order with multiple related " +"subscriptions with no payment method in common." +msgstr "" + #: includes/class-wcs-staging.php:37 msgid "" "Payment processing skipped - renewal order created on %sstaging site%s " @@ -2713,15 +2713,11 @@ msgid "" "related order queries are run." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:66 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:70 msgid "Renew Now" msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:88 -msgid "There was an error with your request to renew. Please try again." -msgstr "" - -#: includes/early-renewal/class-wcs-cart-early-renewal.php:100 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:98 msgid "" "You can not renew this subscription early. Please contact us if you need " "assistance." @@ -2731,23 +2727,23 @@ msgstr "" msgid "Complete checkout to renew now." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:224 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:226 #. translators: placeholder contains a link to the order's edit screen. msgid "Customer successfully renewed early with order %s." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:227 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:229 #. translators: placeholder contains a link to the order's edit screen. msgid "" "Failed to update subscription dates after customer renewed early with order " "%s." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:315 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:317 msgid "Order %s created to record early renewal." msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:370 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:372 msgid "Cancel" msgstr "" @@ -2766,92 +2762,92 @@ msgid "" "the next payment date." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:27 +#: includes/emails/class-wcs-email-cancelled-subscription.php:26 msgid "Cancelled Subscription" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:28 +#: includes/emails/class-wcs-email-cancelled-subscription.php:27 msgid "" "Cancelled Subscription emails are sent when a customer's subscription is " "cancelled (either by a store manager, or the customer)." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:30 +#: includes/emails/class-wcs-email-cancelled-subscription.php:29 msgid "Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:128 +#: includes/emails/class-wcs-email-cancelled-subscription.php:127 #: includes/emails/class-wcs-email-customer-renewal-invoice.php:192 -#: includes/emails/class-wcs-email-expired-subscription.php:126 -#: includes/emails/class-wcs-email-on-hold-subscription.php:126 +#: includes/emails/class-wcs-email-expired-subscription.php:125 +#: includes/emails/class-wcs-email-on-hold-subscription.php:125 msgid "Enable this email notification" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:135 -#: includes/emails/class-wcs-email-expired-subscription.php:133 -#: includes/emails/class-wcs-email-on-hold-subscription.php:133 +#: includes/emails/class-wcs-email-cancelled-subscription.php:134 +#: includes/emails/class-wcs-email-expired-subscription.php:132 +#: includes/emails/class-wcs-email-on-hold-subscription.php:132 #. translators: placeholder is admin email msgid "" "Enter recipients (comma separated) for this email. Defaults to " "%s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:142 -#: includes/emails/class-wcs-email-expired-subscription.php:140 -#: includes/emails/class-wcs-email-on-hold-subscription.php:140 +#: includes/emails/class-wcs-email-cancelled-subscription.php:141 +#: includes/emails/class-wcs-email-expired-subscription.php:139 +#: includes/emails/class-wcs-email-on-hold-subscription.php:139 msgid "" "This controls the email subject line. Leave blank to use the default " "subject: %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:149 -#: includes/emails/class-wcs-email-expired-subscription.php:147 -#: includes/emails/class-wcs-email-on-hold-subscription.php:147 +#: includes/emails/class-wcs-email-cancelled-subscription.php:148 +#: includes/emails/class-wcs-email-expired-subscription.php:146 +#: includes/emails/class-wcs-email-on-hold-subscription.php:146 msgid "" "This controls the main heading contained within the email notification. " "Leave blank to use the default heading: %s." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:156 -#: includes/emails/class-wcs-email-expired-subscription.php:154 -#: includes/emails/class-wcs-email-on-hold-subscription.php:154 +#: includes/emails/class-wcs-email-cancelled-subscription.php:155 +#: includes/emails/class-wcs-email-expired-subscription.php:153 +#: includes/emails/class-wcs-email-on-hold-subscription.php:153 msgid "Choose which format of email to send." msgstr "" -#: includes/emails/class-wcs-email-customer-completed-renewal-order.php:25 +#: includes/emails/class-wcs-email-completed-renewal-order.php:25 msgid "Completed Renewal Order" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-renewal-order.php:26 +#: includes/emails/class-wcs-email-completed-renewal-order.php:26 msgid "" "Renewal order complete emails are sent to the customer when a subscription " "renewal order is marked complete and usually indicates that the item for " "that renewal period has been shipped." msgstr "" -#: includes/emails/class-wcs-email-customer-completed-switch-order.php:26 +#: includes/emails/class-wcs-email-completed-switch-order.php:26 msgid "Subscription Switch Complete" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-switch-order.php:27 +#: includes/emails/class-wcs-email-completed-switch-order.php:27 msgid "" "Subscription switch complete emails are sent to the customer when a " "subscription is switched successfully." msgstr "" -#: includes/emails/class-wcs-email-customer-completed-switch-order.php:30 +#: includes/emails/class-wcs-email-completed-switch-order.php:30 msgid "Your subscription change is complete" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-switch-order.php:31 +#: includes/emails/class-wcs-email-completed-switch-order.php:31 msgid "Your {blogname} subscription change from {order_date} is complete" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-switch-order.php:38 +#: includes/emails/class-wcs-email-completed-switch-order.php:38 msgid "Your subscription change is complete - download your files" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-switch-order.php:39 +#: includes/emails/class-wcs-email-completed-switch-order.php:39 msgid "" "Your {blogname} subscription change from {order_date} is complete - " "download your files" @@ -2878,25 +2874,6 @@ msgstr "" msgid "Automatic payment failed for order {order_number}" msgstr "" -#: includes/emails/class-wcs-email-customer-processing-renewal-order.php:24 -msgid "Processing Renewal order" -msgstr "" - -#: includes/emails/class-wcs-email-customer-processing-renewal-order.php:25 -msgid "" -"This is an order notification sent to the customer after payment for a " -"subscription renewal order is completed. It contains the renewal order " -"details." -msgstr "" - -#: includes/emails/class-wcs-email-customer-processing-renewal-order.php:28 -msgid "Thank you for your order" -msgstr "" - -#: includes/emails/class-wcs-email-customer-processing-renewal-order.php:29 -msgid "Your {blogname} renewal order receipt from {order_date}" -msgstr "" - #: includes/emails/class-wcs-email-customer-renewal-invoice.php:40 msgid "Customer Renewal Invoice" msgstr "" @@ -2918,20 +2895,20 @@ msgstr "" msgid "Invoice for renewal order {order_number}" msgstr "" -#: includes/emails/class-wcs-email-expired-subscription.php:27 +#: includes/emails/class-wcs-email-expired-subscription.php:26 msgid "Expired Subscription" msgstr "" -#: includes/emails/class-wcs-email-expired-subscription.php:28 +#: includes/emails/class-wcs-email-expired-subscription.php:27 msgid "Expired Subscription emails are sent when a customer's subscription expires." msgstr "" -#: includes/emails/class-wcs-email-expired-subscription.php:30 +#: includes/emails/class-wcs-email-expired-subscription.php:29 msgid "Subscription Expired" msgstr "" -#: includes/emails/class-wcs-email-expired-subscription.php:59 -#: includes/emails/class-wcs-email-on-hold-subscription.php:59 +#: includes/emails/class-wcs-email-expired-subscription.php:58 +#: includes/emails/class-wcs-email-on-hold-subscription.php:58 msgid "Subscription argument passed in is not an object." msgstr "" @@ -2968,17 +2945,17 @@ msgstr "" msgid "[{blogname}] Subscription Switched ({order_number}) - {order_date}" msgstr "" -#: includes/emails/class-wcs-email-on-hold-subscription.php:27 +#: includes/emails/class-wcs-email-on-hold-subscription.php:26 msgid "Suspended Subscription" msgstr "" -#: includes/emails/class-wcs-email-on-hold-subscription.php:28 +#: includes/emails/class-wcs-email-on-hold-subscription.php:27 msgid "" "Suspended Subscription emails are sent when a customer manually suspends " "their subscription." msgstr "" -#: includes/emails/class-wcs-email-on-hold-subscription.php:30 +#: includes/emails/class-wcs-email-on-hold-subscription.php:29 msgid "Subscription Suspended" msgstr "" @@ -3003,42 +2980,61 @@ msgid "" "to run {retry_time}" msgstr "" -#: includes/gateways/class-wc-subscriptions-payment-gateways.php:132 +#: includes/emails/class-wcs-email-processing-renewal-order.php:24 +msgid "Processing Renewal order" +msgstr "" + +#: includes/emails/class-wcs-email-processing-renewal-order.php:25 +msgid "" +"This is an order notification sent to the customer after payment for a " +"subscription renewal order is completed. It contains the renewal order " +"details." +msgstr "" + +#: includes/emails/class-wcs-email-processing-renewal-order.php:28 +msgid "Thank you for your order" +msgstr "" + +#: includes/emails/class-wcs-email-processing-renewal-order.php:29 +msgid "Your {blogname} renewal order receipt from {order_date}" +msgstr "" + +#: includes/gateways/class-wc-subscriptions-payment-gateways.php:131 msgid "" "Sorry, it seems there are no available payment methods which support " "subscriptions. Please contact us if you require assistance or wish to make " "alternate arrangements." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:210 +#: includes/gateways/paypal/class-wcs-paypal.php:199 msgid "Unable to find order for PayPal billing agreement." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:267 +#: includes/gateways/paypal/class-wcs-paypal.php:256 msgid "An error occurred, please try again or try an alternate form of payment." msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:374 +#: includes/gateways/paypal/class-wcs-paypal.php:360 #. translators: placeholders are PayPal API error code and PayPal API error #. message msgid "PayPal API error: (%d) %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:379 +#: includes/gateways/paypal/class-wcs-paypal.php:365 #. translators: placeholder is PayPal transaction status message msgid "PayPal Transaction Held: %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:391 +#: includes/gateways/paypal/class-wcs-paypal.php:377 #. translators: placeholder is PayPal transaction status message msgid "PayPal payment declined: %s" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:395 +#: includes/gateways/paypal/class-wcs-paypal.php:381 msgid "PayPal payment approved (ID: %s)" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:448 +#: includes/gateways/paypal/class-wcs-paypal.php:434 msgid "" "Are you sure you want to change the payment method from PayPal standard?\n" "\n" @@ -3052,7 +3048,7 @@ msgid "" "break existing subscriptions." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:110 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:105 #. translators: placeholders are opening and closing link tags. 1$-2$: to docs #. on woocommerce, 3$-4$ to gateway settings on the site msgid "" @@ -3061,7 +3057,7 @@ msgid "" "Subscriptions." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:123 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:116 #. translators: placeholders are opening and closing strong and link tags. #. 1$-2$: strong tags, 3$-8$ link to docs on woocommerce msgid "" @@ -3071,14 +3067,14 @@ msgid "" "%5$sCheck PayPal Account%6$s %3$sLearn more %7$s" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:140 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:132 #. translators: placeholders are opening and closing strong tags. msgid "" "%1$sPayPal Reference Transactions are enabled on your account%2$s. All " "subscription management features are now enabled. Happy selling!" msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:151 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:143 #. translators: placeholders are link opening and closing tags. 1$-2$: to #. gateway settings, 3$-4$: support docs on woocommerce.com msgid "" @@ -3086,7 +3082,7 @@ msgid "" "Please update your %1$sAPI credentials%2$s. %3$sLearn more%4$s." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:164 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:156 #. translators: placeholders are opening and closing link tags. 1$-2$: docs on #. woocommerce, 3$-4$: dismiss link msgid "" @@ -3094,17 +3090,7 @@ msgid "" "subscription IDs. %1$sLearn more%2$s. %3$sDismiss%4$s." msgstr "" -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:179 -msgid "" -"%sA fatal error has occurred when processing a recent subscription payment " -"with PayPal. Please %sopen a new ticket at WooCommerce Support%s " -"immediately to get this resolved.%sIn order to get the quickest possible " -"response please attach a %sTemporary Admin Login%s and a copy of your PHP " -"error logs to your support ticket.%sLast recorded error: %sTo see the full " -"error, view the %s log file from the %sWooCommerce logs screen.%s" -msgstr "" - -#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:269 +#: includes/gateways/paypal/includes/admin/class-wcs-paypal-admin.php:265 msgid "PayPal Subscription ID:" msgstr "" @@ -3134,28 +3120,28 @@ msgstr "" msgid "IPN subscription sign up completed." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:321 -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:398 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:332 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:415 msgid "IPN subscription payment completed." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:360 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:377 msgid "IPN subscription failing payment method changed." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:450 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:467 msgid "IPN subscription suspended." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:473 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:490 msgid "IPN subscription cancelled." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:489 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:506 msgid "IPN subscription payment failure." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:627 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:644 msgid "Invalid PayPal IPN Payload: unable to find matching subscription." msgstr "" @@ -3175,7 +3161,34 @@ msgstr "" msgid "PayPal API error - credentials are incorrect." msgstr "" -#: includes/payment-retry/class-wcs-retry-admin.php:48 +#: includes/gateways/paypal/includes/templates/html-ipn-failure-notice.php:15 +#. translators: $1 and $2 are opening link tags, $3 is a closing link tag. +msgid "" +"A fatal error has occurred while processing a recent subscription payment " +"with PayPal. Please %1$sopen a new ticket at WooCommerce Support%3$s " +"immediately to get this resolved. %2$sLearn more »%3$s" +msgstr "" + +#: includes/gateways/paypal/includes/templates/html-ipn-failure-notice.php:24 +#. translators: $1 and $2 are opening and closing link tags, respectively. +msgid "" +"To resolve this as quickly as possible, please create a %1$stemporary " +"administrator account%2$s with the user email support@prospress.com." +msgstr "" + +#: includes/gateways/paypal/includes/templates/html-ipn-failure-notice.php:29 +msgid "Last recorded error:" +msgstr "" + +#: includes/gateways/paypal/includes/templates/html-ipn-failure-notice.php:36 +#. translators: $1 is the log file name. $2 and $3 are opening and closing link +#. tags, respectively. +msgid "" +"To see the full error, view the %1$s log file from the %2$sWooCommerce logs " +"screen.%3$s." +msgstr "" + +#: includes/payment-retry/class-wcs-retry-admin.php:46 msgid "Automatic Failed Payment Retries" msgstr "" @@ -3209,15 +3222,15 @@ msgid_plural "%d Cancelled Payment Retries" msgstr[0] "" msgstr[1] "" -#: includes/payment-retry/class-wcs-retry-admin.php:135 +#: includes/payment-retry/class-wcs-retry-admin.php:139 msgid "Retry Failed Payments" msgstr "" -#: includes/payment-retry/class-wcs-retry-admin.php:136 +#: includes/payment-retry/class-wcs-retry-admin.php:140 msgid "Enable automatic retry of failed recurring payments" msgstr "" -#: includes/payment-retry/class-wcs-retry-admin.php:140 +#: includes/payment-retry/class-wcs-retry-admin.php:144 msgid "" "Attempt to recover recurring revenue that would otherwise be lost due to " "payment methods being declined only temporarily. %sLearn more%s." @@ -3306,11 +3319,11 @@ msgstr "" msgid "Browser User Agent" msgstr "" -#: includes/privacy/class-wcs-privacy-exporters.php:83 wcs-functions.php:274 +#: includes/privacy/class-wcs-privacy-exporters.php:83 wcs-functions.php:282 msgid "Billing Address" msgstr "" -#: includes/privacy/class-wcs-privacy-exporters.php:84 wcs-functions.php:273 +#: includes/privacy/class-wcs-privacy-exporters.php:84 wcs-functions.php:281 msgid "Shipping Address" msgstr "" @@ -3326,12 +3339,12 @@ msgstr "" msgid "WooCommerce Subscriptions" msgstr "" -#: includes/privacy/class-wcs-privacy.php:46 -#: includes/privacy/class-wcs-privacy.php:47 +#: includes/privacy/class-wcs-privacy.php:43 +#: includes/privacy/class-wcs-privacy.php:44 msgid "Subscriptions Data" msgstr "" -#: includes/privacy/class-wcs-privacy.php:95 +#: includes/privacy/class-wcs-privacy.php:92 msgid "" "By using WooCommerce Subscriptions, you may be storing personal data and " "depending on which third-party payment processors you’re using to take " @@ -3339,24 +3352,24 @@ msgid "" "sources." msgstr "" -#: includes/privacy/class-wcs-privacy.php:97 +#: includes/privacy/class-wcs-privacy.php:94 #. translators: placeholders are opening and closing link tags, linking to #. additional privacy policy documentation. msgid "What we collect and store" msgstr "" -#: includes/privacy/class-wcs-privacy.php:98 +#: includes/privacy/class-wcs-privacy.php:95 msgid "" "For the purposes of processing recurring subscription payments, we store " "the customer's name, billing address, shipping address, email address, " "phone number and credit card/payment details." msgstr "" -#: includes/privacy/class-wcs-privacy.php:99 +#: includes/privacy/class-wcs-privacy.php:96 msgid "What we share with others" msgstr "" -#: includes/privacy/class-wcs-privacy.php:100 +#: includes/privacy/class-wcs-privacy.php:97 msgid "" "What personal information your store shares with external sources depends " "on which third-party payment processor plugins you are using to collect " @@ -3364,7 +3377,7 @@ msgid "" "policies to inform this section of your privacy policy." msgstr "" -#: includes/privacy/class-wcs-privacy.php:102 +#: includes/privacy/class-wcs-privacy.php:99 #. translators: placeholders are opening and closing link tags, linking to #. additional privacy policy documentation. msgid "" @@ -3372,60 +3385,63 @@ msgid "" "see the %sPayPal Privacy Policy%s for more details." msgstr "" -#: includes/privacy/class-wcs-privacy.php:112 +#: includes/privacy/class-wcs-privacy.php:109 msgid "Cancel and remove personal data" msgstr "" -#: includes/privacy/class-wcs-privacy.php:178 +#: includes/privacy/class-wcs-privacy.php:175 msgid "Removed personal data from %d subscription." msgid_plural "Removed personal data from %d subscriptions." msgstr[0] "" msgstr[1] "" -#: includes/privacy/class-wcs-privacy.php:197 +#: includes/privacy/class-wcs-privacy.php:194 #. translators: placeholders are opening and closing tags. msgid "" "%sNote:%s Orders which are related to subscriptions will not be included in " "the orders affected by these settings." msgstr "" -#: includes/privacy/class-wcs-privacy.php:218 -msgid "Remove personal data from subscriptions" +#: includes/privacy/class-wcs-privacy.php:214 +msgid "account erasure request" msgstr "" #: includes/privacy/class-wcs-privacy.php:220 -#. Translators: placeholders are opening and closing link tags linking to the -#. erasure request screen. -msgid "" -"When handling an %saccount erasure request%s, should personal data within " -"subscriptions be retained or removed?" +msgid "Remove personal data from subscriptions" msgstr "" -#: includes/privacy/class-wcs-privacy.php:229 +#: includes/privacy/class-wcs-privacy.php:222 +#. Translators: %s URL to erasure request screen. +msgid "" +"When handling an %s, should personal data within subscriptions be retained " +"or removed?" +msgstr "" + +#: includes/privacy/class-wcs-privacy.php:231 msgid "Retain ended subscriptions" msgstr "" -#: includes/privacy/class-wcs-privacy.php:230 +#: includes/privacy/class-wcs-privacy.php:232 msgid "" "Retain ended subscriptions and their related orders for a specified " "duration before anonymizing the personal data within them." msgstr "" -#: includes/privacy/class-wcs-privacy.php:233 +#: includes/privacy/class-wcs-privacy.php:235 msgid "N/A" msgstr "" -#: includes/privacy/class-wcs-privacy.php:274 +#: includes/privacy/class-wcs-privacy.php:276 msgid "Customers with a subscription are excluded from this setting." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:323 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:330 #. translators: placeholder is a list of version numbers (e.g. "1.3 & 1.4 & #. 1.5") msgid "Database updated to version %s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:346 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:347 #. translators: 1$: number of action scheduler hooks upgraded, 2$: #. "{execution_time}", will be replaced on front end with actual time msgid "" @@ -3433,36 +3449,36 @@ msgid "" "seconds)." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:362 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:359 #. translators: 1$: number of subscriptions upgraded, 2$: "{execution_time}", #. will be replaced on front end with actual time it took msgid "Migrated %1$s subscriptions to the new structure (in %2$s seconds)." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:375 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:372 #. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag msgid "" "Unable to upgrade subscriptions.
    Error: %1$s
    Please refresh the " "page and try again. If problem persists, %2$scontact support%3$s." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:630 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:623 msgid "Welcome to WooCommerce Subscriptions 2.1" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:630 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:623 msgid "About WooCommerce Subscriptions" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:813 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:804 msgid "" "%1$sWarning!%2$s It appears that you have downgraded %1$sWooCommerce " "Subscriptions%2$s from %3$s to %4$s. Downgrading the plugin in this way may " "cause issues. Please update to %3$s or higher, or %5$sopen a new support " -"ticket%6$s for further assistance." +"ticket%6$s for further assistance. %7$sLearn more »%8$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:884 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:892 msgid "" "%1$sWarning!%2$s We discovered an issue in %1$sWooCommerce Subscriptions " "2.3.0 - 2.3.2%2$s that may cause your subscription renewal order and " @@ -4319,11 +4335,11 @@ msgstr "" msgid "\"%s\" is not a valid new order type." msgstr "" -#: includes/wcs-order-functions.php:557 +#: includes/wcs-order-functions.php:560 msgid "Invalid data. No valid subscription / order was passed in." msgstr "" -#: includes/wcs-order-functions.php:561 +#: includes/wcs-order-functions.php:564 msgid "Invalid data. No valid item id was passed in." msgstr "" @@ -4405,6 +4421,40 @@ msgstr "" msgid "Shipping Tax:" msgstr "" +#: templates/admin/html-failed-scheduled-action-notice.php:16 +#. translators: $1 and $2 are opening and closing link tags, respectively. +msgid "" +"An error has occurred while processing a recent subscription related event. " +"Please %1$sopen a new ticket at WooCommerce Support%2$s immediately to get " +"this resolved." +msgid_plural "" +"An error has occurred while processing recent subscription related events. " +"Please %1$sopen a new ticket at WooCommerce Support%2$s immediately to get " +"this resolved." +msgstr[0] "" +msgstr[1] "" + +#: templates/admin/html-failed-scheduled-action-notice.php:29 +#. translators: $1 and $2 are opening and closing link tags, respectively. +msgid "" +"To resolve this error as quickly as possible, please create a %1$stemporary " +"administrator account%2$s with the user email support@prospress.com." +msgstr "" + +#: templates/admin/html-failed-scheduled-action-notice.php:34 +msgid "Affected event:" +msgid_plural "Affected events:" +msgstr[0] "" +msgstr[1] "" + +#: templates/admin/html-failed-scheduled-action-notice.php:41 +#. translators: $1 the log file name $2 and $3 are opening and closing link +#. tags, respectively. +msgid "" +"To see further details, view the %1$s log file from the %2$sWooCommerce " +"logs screen.%2$s" +msgstr "" + #: templates/admin/html-variation-price.php:31 msgid "Subscription trial period:" msgstr "" @@ -4650,87 +4700,87 @@ msgstr "" msgid "Clear" msgstr "" -#: wcs-functions.php:246 +#: wcs-functions.php:254 msgid "Can not get status name. Status is not a string." msgstr "" -#: wcs-functions.php:269 +#: wcs-functions.php:277 msgid "Can not get address type display name. Address type is not a string." msgstr "" -#: wcs-functions.php:332 +#: wcs-functions.php:340 msgid "Date type is not a string." msgstr "" -#: wcs-functions.php:334 +#: wcs-functions.php:342 msgid "Date type can not be an empty string." msgstr "" -#: woocommerce-subscriptions.php:320 +#: woocommerce-subscriptions.php:268 msgid "This is where subscriptions are stored." msgstr "" -#: woocommerce-subscriptions.php:365 +#: woocommerce-subscriptions.php:313 msgid "No Subscriptions found" msgstr "" -#: woocommerce-subscriptions.php:367 +#: woocommerce-subscriptions.php:315 msgid "" "Subscriptions will appear here for you to view and manage once purchased by " "a customer." msgstr "" -#: woocommerce-subscriptions.php:369 +#: woocommerce-subscriptions.php:317 #. translators: placeholders are opening and closing link tags msgid "%sLearn more about managing subscriptions »%s" msgstr "" -#: woocommerce-subscriptions.php:371 +#: woocommerce-subscriptions.php:319 #. translators: placeholders are opening and closing link tags msgid "%sAdd a subscription product »%s" msgstr "" -#: woocommerce-subscriptions.php:525 +#: woocommerce-subscriptions.php:475 msgid "" "A subscription renewal has been removed from your cart. Multiple " "subscriptions can not be purchased at the same time." msgstr "" -#: woocommerce-subscriptions.php:531 +#: woocommerce-subscriptions.php:481 msgid "" "A subscription has been removed from your cart. Due to payment gateway " "restrictions, different subscription products can not be purchased at the " "same time." msgstr "" -#: woocommerce-subscriptions.php:537 +#: woocommerce-subscriptions.php:487 msgid "" "A subscription has been removed from your cart. Products and subscriptions " "can not be purchased at the same time." msgstr "" -#: woocommerce-subscriptions.php:679 woocommerce-subscriptions.php:696 +#: woocommerce-subscriptions.php:629 woocommerce-subscriptions.php:646 #. translators: placeholder is a number, this is for the teens #. translators: placeholder is a number, numbers ending in 4-9, 0 msgid "%sth" msgstr "" -#: woocommerce-subscriptions.php:684 +#: woocommerce-subscriptions.php:634 #. translators: placeholder is a number, numbers ending in 1 msgid "%sst" msgstr "" -#: woocommerce-subscriptions.php:688 +#: woocommerce-subscriptions.php:638 #. translators: placeholder is a number, numbers ending in 2 msgid "%snd" msgstr "" -#: woocommerce-subscriptions.php:692 +#: woocommerce-subscriptions.php:642 #. translators: placeholder is a number, numbers ending in 3 msgid "%srd" msgstr "" -#: woocommerce-subscriptions.php:722 +#: woocommerce-subscriptions.php:672 #. translators: 1$-2$: opening and closing tags, 3$-4$: link tags, #. takes to woocommerce plugin on wp.org, 5$-6$: opening and closing link tags, #. leads to plugins.php in admin @@ -4740,7 +4790,7 @@ msgid "" "%5$sinstall & activate WooCommerce »%6$s" msgstr "" -#: woocommerce-subscriptions.php:725 +#: woocommerce-subscriptions.php:675 #. translators: 1$-2$: opening and closing tags, 3$: minimum supported #. WooCommerce version, 4$-5$: opening and closing link tags, leads to plugin #. admin @@ -4750,11 +4800,11 @@ msgid "" "WooCommerce to version %3$s or newer »%5$s" msgstr "" -#: woocommerce-subscriptions.php:756 +#: woocommerce-subscriptions.php:706 msgid "Variable Subscription" msgstr "" -#: woocommerce-subscriptions.php:899 +#: woocommerce-subscriptions.php:801 msgid "" "%1$sWarning!%2$s We can see the %1$sWooCommerce Subscriptions Early " "Renewal%2$s plugin is active. Version %3$s of %1$sWooCommerce " @@ -4763,33 +4813,35 @@ msgid "" "avoid any conflicts." msgstr "" -#: woocommerce-subscriptions.php:902 +#: woocommerce-subscriptions.php:804 msgid "Installed Plugins" msgstr "" -#: woocommerce-subscriptions.php:975 -#. translators: 1$-2$: opening and closing tags, 3$-4$: opening and -#. closing link tags. Leads to duplicate site article on docs +#: woocommerce-subscriptions.php:873 +#. translators: 1$-2$: opening and closing tags. 3$-4$: opening and +#. closing link tags for learn more. Leads to duplicate site article on docs. +#. 5$-6$: Opening and closing link to production URL. 7$: Production URL . msgid "" "It looks like this site has moved or is a duplicate site. %1$sWooCommerce " "Subscriptions%2$s has disabled automatic payments and subscription related " "emails on this site to prevent duplicate payments from a staging or test " -"environment. %3$sLearn more »%4$s." +"environment. %1$sWooCommerce Subscriptions%2$s considers %5$s%7$s%6$s to be " +"the site's URL. %3$sLearn more »%4$s." msgstr "" -#: woocommerce-subscriptions.php:977 +#: woocommerce-subscriptions.php:882 msgid "Quit nagging me (but don't enable automatic payments)" msgstr "" -#: woocommerce-subscriptions.php:978 +#: woocommerce-subscriptions.php:887 msgid "Enable automatic payments" msgstr "" -#: woocommerce-subscriptions.php:1198 +#: woocommerce-subscriptions.php:1093 msgid "Support" msgstr "" -#: woocommerce-subscriptions.php:1279 +#: woocommerce-subscriptions.php:1176 #. translators: placeholders are opening and closing tags. Leads to docs on #. version 2 msgid "" @@ -4800,14 +4852,14 @@ msgid "" "2.0 »%s" msgstr "" -#: woocommerce-subscriptions.php:1294 +#: woocommerce-subscriptions.php:1191 msgid "" "Warning! You are running version %s of WooCommerce Subscriptions plugin " "code but your database has been upgraded to Subscriptions version 2.0. This " "will cause major problems on your store." msgstr "" -#: woocommerce-subscriptions.php:1295 +#: woocommerce-subscriptions.php:1192 msgid "" "Please upgrade the WooCommerce Subscriptions plugin to version 2.0 or newer " "immediately. If you need assistance, after upgrading to Subscriptions v2.0, " @@ -4815,7 +4867,7 @@ msgid "" msgstr "" #. Plugin URI of the plugin/theme -msgid "http://www.woocommerce.com/products/woocommerce-subscriptions/" +msgid "https://www.woocommerce.com/products/woocommerce-subscriptions/" msgstr "" #. Description of the plugin/theme @@ -4829,10 +4881,10 @@ msgid "Prospress Inc." msgstr "" #. Author URI of the plugin/theme -msgid "http://prospress.com/" +msgid "https://prospress.com/" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:202 +#: includes/admin/class-wc-subscriptions-admin.php:206 #. translators: placeholder is trial period validation message if passed an #. invalid value (e.g. "Trial period can not exceed 4 weeks") msgctxt "Trial period field tooltip on Edit Product administration screen" @@ -4842,12 +4894,12 @@ msgid "" "subscription. %s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:215 +#: includes/admin/class-wc-subscriptions-admin.php:219 msgctxt "example price" msgid "e.g. 5.90" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:249 +#: includes/admin/class-wc-subscriptions-admin.php:253 #: templates/admin/deprecated/html-variation-price.php:31 #: templates/admin/deprecated/html-variation-price.php:86 #: templates/admin/html-variation-price.php:21 @@ -4856,7 +4908,7 @@ msgctxt "example price" msgid "e.g. 9.90" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:786 +#: includes/admin/class-wc-subscriptions-admin.php:790 #. translators: placeholders are for HTML tags. They are 1$: "

    ", 2$: #. "

    ", 3$: "

    ", 4$: "", 5$: "", 6$: "", 7$: "", 8$: #. "

    " @@ -4867,7 +4919,7 @@ msgid "" "%6$sVariable subscription%7$s.%8$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:788 +#: includes/admin/class-wc-subscriptions-admin.php:792 #. translators: placeholders are for HTML tags. They are 1$: "

    ", 2$: #. "

    ", 3$: "

    ", 4$: "

    " msgctxt "used in admin pointer script params in javascript as price pointer content" @@ -4877,17 +4929,17 @@ msgid "" "sign-up fee and free trial.%4$s" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1125 +#: includes/admin/class-wc-subscriptions-admin.php:1124 msgctxt "option section heading" msgid "Renewals" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1157 +#: includes/admin/class-wc-subscriptions-admin.php:1156 msgctxt "options section heading" msgid "Miscellaneous" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1165 +#: includes/admin/class-wc-subscriptions-admin.php:1164 msgctxt "there's a number immediately in front of this text" msgid "suspensions per billing period." msgstr "" @@ -4897,26 +4949,26 @@ msgctxt "there's a number immediately in front of this text" msgid "days prior to Renewal Day" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1446 -#: includes/admin/class-wcs-admin-system-status.php:92 +#: includes/admin/class-wc-subscriptions-admin.php:1445 +#: includes/admin/class-wcs-admin-system-status.php:93 msgctxt "label that indicates whether debugging is turned on for the plugin" msgid "WCS_DEBUG" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1452 -#: includes/admin/class-wcs-admin-system-status.php:104 +#: includes/admin/class-wc-subscriptions-admin.php:1451 +#: includes/admin/class-wcs-admin-system-status.php:107 msgctxt "Live or Staging, Label on WooCommerce -> System Status page" msgid "Subscriptions Mode" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1453 -#: includes/admin/class-wcs-admin-system-status.php:106 +#: includes/admin/class-wc-subscriptions-admin.php:1452 +#: includes/admin/class-wcs-admin-system-status.php:109 msgctxt "refers to staging site" msgid "Staging" msgstr "" -#: includes/admin/class-wc-subscriptions-admin.php:1453 -#: includes/admin/class-wcs-admin-system-status.php:106 +#: includes/admin/class-wc-subscriptions-admin.php:1452 +#: includes/admin/class-wcs-admin-system-status.php:109 msgctxt "refers to live site" msgid "Live" msgstr "" @@ -4972,72 +5024,82 @@ msgctxt "Subscription title on admin table. (e.g.: #211 for John Doe)" msgid "%1$s#%2$s%3$s for %4$s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:878 +#: includes/admin/class-wcs-admin-post-types.php:889 #. translators: placeholder is previous post title msgctxt "used in post updated messages" msgid "Subscription restored to revision from %s" msgstr "" -#: includes/admin/class-wcs-admin-post-types.php:883 +#: includes/admin/class-wcs-admin-post-types.php:894 msgctxt "used in \"Subscription scheduled for \"" msgid "M j, Y @ G:i" msgstr "" #: includes/admin/class-wcs-admin-system-status.php:119 +msgctxt "Live URL, Label on WooCommerce -> System Status page" +msgid "Subscriptions Live URL" +msgstr "" + +#: includes/admin/class-wcs-admin-system-status.php:135 msgctxt "label for the system status page" msgid "Subscriptions Template Theme Overrides" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:204 +#: includes/admin/class-wcs-admin-system-status.php:220 msgctxt "label for the system status page" msgid "Subscription Statuses" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:225 +#: includes/admin/class-wcs-admin-system-status.php:241 msgctxt "label for the system status page" msgid "WooCommerce Account Connected" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:248 +#: includes/admin/class-wcs-admin-system-status.php:264 msgctxt "label for the system status page" msgid "Active Product Key" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:278 +#: includes/admin/class-wcs-admin-system-status.php:294 msgctxt "label for the system status page" msgid "Other" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:312 +#: includes/admin/class-wcs-admin-system-status.php:328 msgctxt "label for the system status page" msgid "PayPal Reference Transactions Enabled" msgstr "" -#: includes/admin/class-wcs-admin-system-status.php:340 +#: includes/admin/class-wcs-admin-system-status.php:356 msgctxt "label for the system status page" msgid "Country / State" msgstr "" -#: includes/payment-retry/class-wcs-retry-admin.php:160 +#: includes/payment-retry/class-wcs-retry-admin.php:166 msgctxt "label for the system status page" msgid "Custom Retry Rules" msgstr "" -#: includes/payment-retry/class-wcs-retry-admin.php:168 +#: includes/payment-retry/class-wcs-retry-admin.php:174 msgctxt "label for the system status page" msgid "Custom Retry Rule Class" msgstr "" -#: includes/payment-retry/class-wcs-retry-admin.php:176 +#: includes/payment-retry/class-wcs-retry-admin.php:182 msgctxt "label for the system status page" msgid "Custom Raw Retry Rule" msgstr "" -#: includes/payment-retry/class-wcs-retry-admin.php:184 +#: includes/payment-retry/class-wcs-retry-admin.php:190 msgctxt "label for the system status page" msgid "Custom Retry Rule" msgstr "" +#: includes/payment-retry/class-wcs-retry-admin.php:198 +msgctxt "label for the system status page" +msgid "Retries Migration Status" +msgstr "" + #: includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php:78 msgctxt "relation to order" msgid "Resubscribed Subscription" @@ -5084,8 +5146,8 @@ msgstr "" #: includes/admin/meta-boxes/views/html-related-orders-row.php:19 #: includes/class-wc-subscriptions-renewal-order.php:157 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:217 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:314 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:219 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:316 #: templates/myaccount/my-subscriptions.php:38 #: templates/myaccount/related-orders.php:39 #: templates/myaccount/related-subscriptions.php:32 @@ -5093,6 +5155,8 @@ msgctxt "hash before order number" msgid "#%s" msgstr "" +#: includes/class-wc-subscriptions-addresses.php:206 +#: includes/class-wc-subscriptions-change-payment-gateway.php:616 #: includes/class-wcs-query.php:102 msgctxt "hash before order number" msgid "Subscription #%s" @@ -5114,26 +5178,26 @@ msgctxt "table heading" msgid "Total" msgstr "" -#: includes/class-wcs-retry-manager.php:122 +#: includes/class-wcs-retry-manager.php:119 msgctxt "table heading" msgid "Renewal Payment Retry" msgstr "" #: templates/emails/cancelled-subscription.php:28 #: templates/emails/expired-subscription.php:28 -#: templates/emails/on-hold-subscription.php:28 wcs-functions.php:295 +#: templates/emails/on-hold-subscription.php:28 wcs-functions.php:303 msgctxt "table heading" msgid "Last Order Date" msgstr "" #: templates/emails/subscription-info.php:19 -#: templates/myaccount/subscription-details.php:22 wcs-functions.php:292 +#: templates/myaccount/subscription-details.php:22 wcs-functions.php:300 msgctxt "table heading" msgid "Start Date" msgstr "" #: templates/emails/subscription-info.php:20 -#: templates/myaccount/subscription-details.php:28 wcs-functions.php:297 +#: templates/myaccount/subscription-details.php:28 wcs-functions.php:305 msgctxt "table heading" msgid "End Date" msgstr "" @@ -5146,22 +5210,22 @@ msgstr "" #: templates/myaccount/my-subscriptions.php:27 #: templates/myaccount/my-subscriptions.php:44 #: templates/myaccount/related-subscriptions.php:22 -#: templates/myaccount/related-subscriptions.php:38 wcs-functions.php:294 +#: templates/myaccount/related-subscriptions.php:38 wcs-functions.php:302 msgctxt "table heading" msgid "Next Payment" msgstr "" -#: wcs-functions.php:293 +#: wcs-functions.php:301 msgctxt "table heading" msgid "Trial End" msgstr "" -#: wcs-functions.php:296 +#: wcs-functions.php:304 msgctxt "table heading" msgid "Cancelled Date" msgstr "" -#: includes/admin/reports/class-wcs-report-cache-manager.php:339 +#: includes/admin/reports/class-wcs-report-cache-manager.php:326 msgctxt "Whether the Report Cache has been enabled" msgid "Report Cache Enabled" msgstr "" @@ -5187,35 +5251,41 @@ msgctxt "API error message when editing the order failed" msgid "Edit subscription failed with error: %s" msgstr "" -#: includes/api/legacy/class-wc-api-subscriptions.php:608 +#: includes/api/legacy/class-wc-api-subscriptions.php:606 msgctxt "API response confirming order note deleted from a subscription" msgid "Permanently deleted subscription note" msgstr "" -#: includes/class-wc-subscription.php:1103 +#: includes/class-wc-subscription.php:1118 msgctxt "original denotes there is no date to display" msgid "-" msgstr "" -#: includes/class-wc-subscription.php:2237 +#: includes/class-wc-subscription.php:2238 #. translators: placeholder is date type (e.g. "end", "next_payment"...) msgctxt "appears in an error message if date is wrong format" msgid "Invalid %s date. The date must be of the format: \"Y-m-d H:i:s\"." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:300 +#: includes/class-wc-subscriptions-addresses.php:211 +msgctxt "change billing or shipping address" +msgid "Change %s address" +msgstr "" + +#: includes/class-wc-subscriptions-change-payment-gateway.php:315 msgctxt "label on button, imperative" msgid "Change Payment" msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:426 +#: includes/class-wc-subscriptions-change-payment-gateway.php:441 msgctxt "%1$s: old payment title, %2$s: new payment title" msgid "" "Payment method changed from \"%1$s\" to \"%2$s\" by the subscriber from " "their account page." msgstr "" -#: includes/class-wc-subscriptions-change-payment-gateway.php:552 +#: includes/class-wc-subscriptions-change-payment-gateway.php:587 +#: includes/class-wc-subscriptions-change-payment-gateway.php:621 msgctxt "the page title of the change payment method form" msgid "Change Payment Method" msgstr "" @@ -5227,33 +5297,33 @@ msgctxt "used in order note as reason for why subscription status changed" msgid "Subscription renewal payment due:" msgstr "" -#: includes/class-wcs-retry-manager.php:302 +#: includes/class-wcs-retry-manager.php:300 msgctxt "used in order note as reason for why subscription status changed" msgid "Subscription renewal payment retry:" msgstr "" -#: includes/early-renewal/class-wcs-cart-early-renewal.php:147 -#: includes/early-renewal/class-wcs-cart-early-renewal.php:174 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:149 +#: includes/early-renewal/class-wcs-cart-early-renewal.php:176 msgctxt "used in order note as reason for why subscription status changed" msgid "Customer requested to renew early:" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1040 wcs-functions.php:225 +#: includes/class-wc-subscriptions-manager.php:1040 wcs-functions.php:233 msgctxt "Subscription status" msgid "Active" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1043 wcs-functions.php:227 +#: includes/class-wc-subscriptions-manager.php:1043 wcs-functions.php:235 msgctxt "Subscription status" msgid "Cancelled" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1046 wcs-functions.php:229 +#: includes/class-wc-subscriptions-manager.php:1046 wcs-functions.php:237 msgctxt "Subscription status" msgid "Expired" msgstr "" -#: includes/class-wc-subscriptions-manager.php:1049 wcs-functions.php:224 +#: includes/class-wc-subscriptions-manager.php:1049 wcs-functions.php:232 msgctxt "Subscription status" msgid "Pending" msgstr "" @@ -5268,17 +5338,17 @@ msgctxt "Subscription status" msgid "On-hold" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:2587 wcs-functions.php:228 +#: includes/class-wc-subscriptions-switcher.php:2504 wcs-functions.php:236 msgctxt "Subscription status" msgid "Switched" msgstr "" -#: wcs-functions.php:226 +#: wcs-functions.php:234 msgctxt "Subscription status" msgid "On hold" msgstr "" -#: wcs-functions.php:230 +#: wcs-functions.php:238 msgctxt "Subscription status" msgid "Pending Cancellation" msgstr "" @@ -5399,30 +5469,29 @@ msgctxt "when to prorate first payment / subscription length" msgid "Never (charge the full recurring amount at sign-up)" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1782 +#: includes/class-wc-subscriptions-switcher.php:1798 msgctxt "a switch order" msgid "Downgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1785 +#: includes/class-wc-subscriptions-switcher.php:1801 msgctxt "a switch order" msgid "Upgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1788 +#: includes/class-wc-subscriptions-switcher.php:1804 msgctxt "a switch order" msgid "Crossgrade" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1793 +#: includes/class-wc-subscriptions-switcher.php:1809 #. translators: %1: product subtotal, %2: HTML span tag, %3: direction #. (upgrade, downgrade, crossgrade), %4: closing HTML span tag msgctxt "product subtotal string" msgid "%1$s %2$s(%3$s)%4$s" msgstr "" -#: includes/class-wc-subscriptions-switcher.php:1909 -#: includes/class-wc-subscriptions-switcher.php:2251 +#: includes/class-wc-subscriptions-switcher.php:1920 #. translators: 1$: old item, 2$: new item when switching msgctxt "used in order notes" msgid "Customer switched from: %1$s to %2$s." @@ -5453,7 +5522,7 @@ msgctxt "input field placeholder for day field for annual subscriptions" msgid "Day" msgstr "" -#: includes/class-wcs-cart-renewal.php:770 +#: includes/class-wcs-cart-renewal.php:682 msgctxt "" "Used in WooCommerce by removed item notification: \"_All linked " "subscription items were_ removed. Undo?\" Filter for item title." @@ -5491,25 +5560,25 @@ msgctxt "used in order note" msgid "Customer removed \"%1$s\" (Product ID: #%2$d) via the My Account page." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:408 -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:417 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:425 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:434 #. translators: placeholder is payment status (e.g. "completed") msgctxt "used in order note" msgid "IPN subscription payment %s." msgstr "" -#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:421 +#: includes/gateways/paypal/includes/class-wcs-paypal-standard-ipn-handler.php:438 #. translators: placeholder is payment status (e.g. "completed") msgctxt "used in order note" msgid "IPN subscription payment %s for reason: %s." msgstr "" -#: includes/class-wcs-retry-manager.php:232 +#: includes/class-wcs-retry-manager.php:229 msgctxt "used in order note as reason for why status changed" msgid "Retry rule applied:" msgstr "" -#: includes/class-wcs-retry-manager.php:298 +#: includes/class-wcs-retry-manager.php:296 msgctxt "used in order note as reason for why order status changed" msgid "Subscription renewal payment retry:" msgstr "" @@ -5544,90 +5613,90 @@ msgctxt "Notice displayed to user confirming their action." msgid "Your subscription has been cancelled." msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:32 +#: includes/emails/class-wcs-email-cancelled-subscription.php:31 #. translators: placeholder is {blogname}, a variable that will be substituted #. when email is sent out msgctxt "default email subject for cancelled emails sent to the admin" msgid "[%s] Subscription Cancelled" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:126 +#: includes/emails/class-wcs-email-cancelled-subscription.php:125 #: includes/emails/class-wcs-email-customer-renewal-invoice.php:190 -#: includes/emails/class-wcs-email-expired-subscription.php:124 -#: includes/emails/class-wcs-email-on-hold-subscription.php:124 +#: includes/emails/class-wcs-email-expired-subscription.php:123 +#: includes/emails/class-wcs-email-on-hold-subscription.php:123 msgctxt "an email notification" msgid "Enable/Disable" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:132 -#: includes/emails/class-wcs-email-expired-subscription.php:130 -#: includes/emails/class-wcs-email-on-hold-subscription.php:130 +#: includes/emails/class-wcs-email-cancelled-subscription.php:131 +#: includes/emails/class-wcs-email-expired-subscription.php:129 +#: includes/emails/class-wcs-email-on-hold-subscription.php:129 msgctxt "of an email" msgid "Recipient(s)" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:140 -#: includes/emails/class-wcs-email-expired-subscription.php:138 -#: includes/emails/class-wcs-email-on-hold-subscription.php:138 +#: includes/emails/class-wcs-email-cancelled-subscription.php:139 +#: includes/emails/class-wcs-email-expired-subscription.php:137 +#: includes/emails/class-wcs-email-on-hold-subscription.php:137 msgctxt "of an email" msgid "Subject" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:147 -#: includes/emails/class-wcs-email-expired-subscription.php:145 -#: includes/emails/class-wcs-email-on-hold-subscription.php:145 +#: includes/emails/class-wcs-email-cancelled-subscription.php:146 +#: includes/emails/class-wcs-email-expired-subscription.php:144 +#: includes/emails/class-wcs-email-on-hold-subscription.php:144 msgctxt "" "Name the setting that controls the main heading contained within the email " "notification" msgid "Email Heading" msgstr "" -#: includes/emails/class-wcs-email-cancelled-subscription.php:154 -#: includes/emails/class-wcs-email-expired-subscription.php:152 -#: includes/emails/class-wcs-email-on-hold-subscription.php:152 +#: includes/emails/class-wcs-email-cancelled-subscription.php:153 +#: includes/emails/class-wcs-email-expired-subscription.php:151 +#: includes/emails/class-wcs-email-on-hold-subscription.php:151 msgctxt "text, html or multipart" msgid "Email type" msgstr "" +#: includes/emails/class-wcs-email-cancelled-subscription.php:159 +#: includes/emails/class-wcs-email-expired-subscription.php:157 +#: includes/emails/class-wcs-email-on-hold-subscription.php:157 +msgctxt "email type" +msgid "Plain text" +msgstr "" + #: includes/emails/class-wcs-email-cancelled-subscription.php:160 #: includes/emails/class-wcs-email-expired-subscription.php:158 #: includes/emails/class-wcs-email-on-hold-subscription.php:158 msgctxt "email type" -msgid "Plain text" +msgid "HTML" msgstr "" #: includes/emails/class-wcs-email-cancelled-subscription.php:161 #: includes/emails/class-wcs-email-expired-subscription.php:159 #: includes/emails/class-wcs-email-on-hold-subscription.php:159 msgctxt "email type" -msgid "HTML" -msgstr "" - -#: includes/emails/class-wcs-email-cancelled-subscription.php:162 -#: includes/emails/class-wcs-email-expired-subscription.php:160 -#: includes/emails/class-wcs-email-on-hold-subscription.php:160 -msgctxt "email type" msgid "Multipart" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-renewal-order.php:29 +#: includes/emails/class-wcs-email-completed-renewal-order.php:29 msgctxt "Default email heading for email to customer on completed renewal order" msgid "Your renewal order is complete" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-renewal-order.php:31 +#: includes/emails/class-wcs-email-completed-renewal-order.php:31 #. translators: $1: {blogname}, $2: {order_date}, variables that will be #. substituted when email is sent out msgctxt "Default email subject for email to customer on completed renewal order" msgid "Your %1$s renewal order from %2$s is complete" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-renewal-order.php:38 +#: includes/emails/class-wcs-email-completed-renewal-order.php:38 msgctxt "Default email heading for email with downloadable files in it" msgid "Your subscription renewal order is complete - download your files" msgstr "" -#: includes/emails/class-wcs-email-customer-completed-renewal-order.php:40 +#: includes/emails/class-wcs-email-completed-renewal-order.php:40 #. translators: $1: {blogname}, $2: {order_date}, variables will be substituted #. when email is sent out msgctxt "Default email subject for email with downloadable files in it" @@ -5636,21 +5705,21 @@ msgid "" "files" msgstr "" -#: includes/emails/class-wcs-email-expired-subscription.php:32 +#: includes/emails/class-wcs-email-expired-subscription.php:31 #. translators: placeholder is {blogname}, a variable that will be substituted #. when email is sent out msgctxt "default email subject for expired emails sent to the admin" msgid "[%s] Subscription Expired" msgstr "" -#: includes/emails/class-wcs-email-on-hold-subscription.php:32 +#: includes/emails/class-wcs-email-on-hold-subscription.php:31 #. translators: placeholder is {blogname}, a variable that will be substituted #. when email is sent out msgctxt "default email subject for suspended emails sent to the admin" msgid "[%s] Subscription Suspended" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:350 +#: includes/gateways/paypal/class-wcs-paypal.php:336 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:208 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:315 #: includes/gateways/paypal/includes/class-wcs-paypal-reference-transaction-api-request.php:326 @@ -5665,7 +5734,7 @@ msgctxt "" msgid "#" msgstr "" -#: includes/gateways/paypal/class-wcs-paypal.php:562 +#: includes/gateways/paypal/class-wcs-paypal.php:532 msgctxt "" "used in User Agent data sent to PayPal to help identify where a payment " "came from" @@ -5732,21 +5801,21 @@ msgctxt "Admin menu name" msgid "Renewal Payment Retries" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:334 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:338 #. translators: placeholder is number of upgraded subscriptions msgctxt "used in the subscriptions upgrader" msgid "Marked %s subscription products as \"sold individually\"." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:365 -#: includes/upgrades/class-wc-subscriptions-upgrader.php:415 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:362 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:408 #. translators: placeholder is "{time_left}", will be replaced on front end #. with actual time msgctxt "Message that gets sent to front end." msgid "Estimated time left (minutes:seconds): %s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:394 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:387 #. translators: placeholder is the number of subscriptions repaired msgctxt "Repair message that gets sent to front end." msgid "" @@ -5754,7 +5823,7 @@ msgid "" "customer notes." msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:400 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:393 #. translators: placeholder is number of subscriptions that were checked and #. did not need repairs. There's a space at the beginning! msgctxt "Repair message that gets sent to front end." @@ -5763,14 +5832,14 @@ msgid_plural "%d other subscriptions were checked and did not need any repairs." msgstr[0] "" msgstr[1] "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:404 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:397 #. translators: placeholder is "{execution_time}", which will be replaced on #. front end with actual time msgctxt "Repair message that gets sent to front end." msgid "(in %s seconds)" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:407 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:400 #. translators: $1: "Repaired x subs with incorrect dates...", $2: "X others #. were checked and no repair needed", $3: "(in X seconds)". Ordering for RTL #. languages. @@ -5778,7 +5847,7 @@ msgctxt "The assembled repair message that gets sent to front end." msgid "%1$s%2$s %3$s" msgstr "" -#: includes/upgrades/class-wc-subscriptions-upgrader.php:426 +#: includes/upgrades/class-wc-subscriptions-upgrader.php:419 #. translators: 1$: error message, 2$: opening link tag, 3$: closing link tag msgctxt "Error message that gets sent to front end when upgrading Subscriptions" msgid "" @@ -5792,7 +5861,7 @@ msgid "2.3" msgstr "" #: includes/upgrades/templates/wcs-about-2-0.php:36 -#: woocommerce-subscriptions.php:1197 +#: woocommerce-subscriptions.php:1092 msgctxt "short for documents" msgid "Docs" msgstr "" @@ -5863,7 +5932,7 @@ msgctxt "" msgid "Invalid data. Type of copy is not a string." msgstr "" -#: includes/wcs-order-functions.php:340 wcs-functions.php:155 +#: includes/wcs-order-functions.php:340 wcs-functions.php:161 #. translators: Order date parsed by strftime msgctxt "Used in subscription post title. \"Subscription renewal order - \"" msgid "%b %d, %Y @ %I:%M %p" @@ -6249,89 +6318,96 @@ msgctxt "date on subscription updates list. Will be localized" msgid "l jS \\o\\f F Y, h:ia" msgstr "" -#: wcs-functions.php:129 +#: wcs-functions.php:130 +msgctxt "Error message while creating a subscription" +msgid "" +"Invalid created date. The date must be a string and of the format: \"Y-m-d " +"H:i:s\"." +msgstr "" + +#: wcs-functions.php:132 +msgctxt "Error message while creating a subscription" +msgid "Subscription created date must be before current day." +msgstr "" + +#: wcs-functions.php:137 msgctxt "Error message while creating a subscription" msgid "Invalid date. The date must be a string and of the format: \"Y-m-d H:i:s\"." msgstr "" -#: wcs-functions.php:131 -msgctxt "Error message while creating a subscription" -msgid "Subscription start date must be before current day." -msgstr "" - -#: wcs-functions.php:136 +#: wcs-functions.php:142 msgctxt "Error message while creating a subscription" msgid "Invalid subscription customer_id." msgstr "" -#: wcs-functions.php:157 +#: wcs-functions.php:163 #. translators: placeholder is order date parsed by strftime msgctxt "The post title for the new subscription" msgid "Subscription – %s" msgstr "" -#: woocommerce-subscriptions.php:307 +#: woocommerce-subscriptions.php:255 msgctxt "custom post type setting" msgid "Add Subscription" msgstr "" -#: woocommerce-subscriptions.php:308 +#: woocommerce-subscriptions.php:256 msgctxt "custom post type setting" msgid "Add New Subscription" msgstr "" -#: woocommerce-subscriptions.php:309 +#: woocommerce-subscriptions.php:257 msgctxt "custom post type setting" msgid "Edit" msgstr "" -#: woocommerce-subscriptions.php:310 +#: woocommerce-subscriptions.php:258 msgctxt "custom post type setting" msgid "Edit Subscription" msgstr "" -#: woocommerce-subscriptions.php:311 +#: woocommerce-subscriptions.php:259 msgctxt "custom post type setting" msgid "New Subscription" msgstr "" -#: woocommerce-subscriptions.php:312 woocommerce-subscriptions.php:313 +#: woocommerce-subscriptions.php:260 woocommerce-subscriptions.php:261 msgctxt "custom post type setting" msgid "View Subscription" msgstr "" -#: woocommerce-subscriptions.php:316 +#: woocommerce-subscriptions.php:264 msgctxt "custom post type setting" msgid "No Subscriptions found in trash" msgstr "" -#: woocommerce-subscriptions.php:317 +#: woocommerce-subscriptions.php:265 msgctxt "custom post type setting" msgid "Parent Subscriptions" msgstr "" -#: woocommerce-subscriptions.php:385 +#: woocommerce-subscriptions.php:333 msgctxt "post status label including post count" msgid "Active (%s)" msgid_plural "Active (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:386 +#: woocommerce-subscriptions.php:334 msgctxt "post status label including post count" msgid "Switched (%s)" msgid_plural "Switched (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:387 +#: woocommerce-subscriptions.php:335 msgctxt "post status label including post count" msgid "Expired (%s)" msgid_plural "Expired (%s)" msgstr[0] "" msgstr[1] "" -#: woocommerce-subscriptions.php:388 +#: woocommerce-subscriptions.php:336 msgctxt "post status label including post count" msgid "Pending Cancellation (%s)" msgid_plural "Pending Cancellation (%s)" diff --git a/readme.txt b/readme.txt old mode 100755 new mode 100644 diff --git a/templates/admin/deprecated/html-variation-price.php b/templates/admin/deprecated/html-variation-price.php old mode 100755 new mode 100644 diff --git a/templates/admin/deprecated/html-variation-synchronisation.php b/templates/admin/deprecated/html-variation-synchronisation.php old mode 100755 new mode 100644 diff --git a/templates/admin/deprecated/order-shipping-html.php b/templates/admin/deprecated/order-shipping-html.php old mode 100755 new mode 100644 diff --git a/templates/admin/deprecated/order-tax-html.php b/templates/admin/deprecated/order-tax-html.php old mode 100755 new mode 100644 diff --git a/templates/admin/html-admin-notice.php b/templates/admin/html-admin-notice.php old mode 100755 new mode 100644 diff --git a/templates/admin/html-failed-scheduled-action-notice.php b/templates/admin/html-failed-scheduled-action-notice.php new file mode 100644 index 0000000..d98d6bc --- /dev/null +++ b/templates/admin/html-failed-scheduled-action-notice.php @@ -0,0 +1,47 @@ + +

    ', + '' + );?> +

    +

    ', + '' + );?> +

    + + array( 'href' => array() ) ) ); ?> + +

    failed-scheduled-actions', + '', + '' + );?> +

    + diff --git a/templates/admin/html-variation-price.php b/templates/admin/html-variation-price.php old mode 100755 new mode 100644 index a860283..02f8268 --- a/templates/admin/html-variation-price.php +++ b/templates/admin/html-variation-price.php @@ -28,7 +28,7 @@ if ( ! defined( 'ABSPATH' ) ) { - + chosen, true ); ?> data-order_button_text="" /> get_id() ) : $subscription->get_view_order_url() ) . "\n"; // translators: placeholder is localised start date - echo sprintf( _x( 'Start Date: %s', 'in plain emails for subscription information', 'woocommerce-subscriptions' ), date_i18n( wc_date_format(), $subscription->get_time( 'date_created', 'site' ) ) ) . "\n"; + echo sprintf( _x( 'Start Date: %s', 'in plain emails for subscription information', 'woocommerce-subscriptions' ), date_i18n( wc_date_format(), $subscription->get_time( 'start_date', 'site' ) ) ) . "\n"; $end_date = ( 0 < $subscription->get_time( 'end' ) ) ? date_i18n( wc_date_format(), $subscription->get_time( 'end', 'site' ) ) : _x( 'When Cancelled', 'Used as end date for an indefinite subscription', 'woocommerce-subscriptions' ); // translators: placeholder is localised end date, or "when cancelled" diff --git a/templates/emails/subscription-info.php b/templates/emails/subscription-info.php old mode 100755 new mode 100644 index e1f55d2..dc04e43 --- a/templates/emails/subscription-info.php +++ b/templates/emails/subscription-info.php @@ -25,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) { get_order_number() ) ); ?> - get_time( 'date_created', 'site' ) ) ); ?> + get_time( 'start_date', 'site' ) ) ); ?> get_time( 'end' ) ) ? date_i18n( wc_date_format(), $subscription->get_time( 'end', 'site' ) ) : _x( 'When Cancelled', 'Used as end date for an indefinite subscription', 'woocommerce-subscriptions' ) ); ?> get_formatted_order_total() ); ?> diff --git a/templates/myaccount/my-subscriptions.php b/templates/myaccount/my-subscriptions.php old mode 100755 new mode 100644 diff --git a/templates/myaccount/related-orders.php b/templates/myaccount/related-orders.php old mode 100755 new mode 100644 diff --git a/templates/myaccount/related-subscriptions.php b/templates/myaccount/related-subscriptions.php old mode 100755 new mode 100644 diff --git a/templates/myaccount/subscription-details.php b/templates/myaccount/subscription-details.php old mode 100755 new mode 100644 index 65504bf..2b8caf5 --- a/templates/myaccount/subscription-details.php +++ b/templates/myaccount/subscription-details.php @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { - get_date_to_display( 'date_created' ) ); ?> + get_date_to_display( 'start_date' ) ); ?> _x( 'Last Order Date', 'admin subscription table header', 'woocommerce-subscriptions' ), diff --git a/templates/myaccount/subscription-totals.php b/templates/myaccount/subscription-totals.php old mode 100755 new mode 100644 diff --git a/templates/myaccount/subscriptions.php b/templates/myaccount/subscriptions.php old mode 100755 new mode 100644 diff --git a/templates/myaccount/view-subscription.php b/templates/myaccount/view-subscription.php old mode 100755 new mode 100644 diff --git a/templates/single-product/add-to-cart/subscription.php b/templates/single-product/add-to-cart/subscription.php old mode 100755 new mode 100644 diff --git a/templates/single-product/add-to-cart/variable-subscription.php b/templates/single-product/add-to-cart/variable-subscription.php old mode 100755 new mode 100644 diff --git a/wcs-functions.php b/wcs-functions.php old mode 100755 new mode 100644 index f3b745c..9bd5adc --- a/wcs-functions.php +++ b/wcs-functions.php @@ -12,24 +12,23 @@ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly } -require_once( 'includes/class-wcs-select2.php' ); -require_once( 'includes/wcs-deprecated-functions.php' ); -require_once( 'includes/wcs-compatibility-functions.php' ); -require_once( 'includes/wcs-conditional-functions.php' ); -require_once( 'includes/wcs-formatting-functions.php' ); -require_once( 'includes/wcs-product-functions.php' ); -require_once( 'includes/wcs-cart-functions.php' ); -require_once( 'includes/wcs-order-functions.php' ); -require_once( 'includes/wcs-time-functions.php' ); -require_once( 'includes/wcs-user-functions.php' ); -require_once( 'includes/wcs-helper-functions.php' ); -require_once( 'includes/wcs-renewal-functions.php' ); -require_once( 'includes/wcs-resubscribe-functions.php' ); -require_once( 'includes/wcs-switch-functions.php' ); -require_once( 'includes/wcs-limit-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-deprecated-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-compatibility-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-conditional-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-formatting-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-product-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-cart-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-order-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-time-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-user-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-helper-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-renewal-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-resubscribe-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-switch-functions.php' ); +require_once( dirname( __FILE__ ) . '/includes/wcs-limit-functions.php' ); if ( is_admin() ) { - require_once( 'includes/admin/wcs-admin-functions.php' ); + require_once( dirname( __FILE__ ) . '/includes/admin/wcs-admin-functions.php' ); } /** @@ -101,12 +100,13 @@ function wcs_get_subscription( $the_subscription ) { */ function wcs_create_subscription( $args = array() ) { + $now = gmdate( 'Y-m-d H:i:s' ); $order = ( isset( $args['order_id'] ) ) ? wc_get_order( $args['order_id'] ) : null; if ( ! empty( $order ) ) { - $default_start_date = wcs_get_datetime_utc_string( wcs_get_objects_property( $order, 'date_created' ) ); + $default_start_date = wcs_get_datetime_utc_string( wcs_get_objects_property( $order, 'date_created' ) ); } else { - $default_start_date = gmdate( 'Y-m-d H:i:s' ); + $default_start_date = ( isset( $args['date_created'] ) ) ? $args['date_created'] : $now; } $default_args = array( @@ -115,6 +115,7 @@ function wcs_create_subscription( $args = array() ) { 'customer_note' => null, 'customer_id' => ( ! empty( $order ) ) ? $order->get_user_id() : null, 'start_date' => $default_start_date, + 'date_created' => $now, 'created_via' => ( ! empty( $order ) ) ? wcs_get_objects_property( $order, 'created_via' ) : '', 'order_version' => ( ! empty( $order ) ) ? wcs_get_objects_property( $order, 'version' ) : WC_VERSION, 'currency' => ( ! empty( $order ) ) ? wcs_get_objects_property( $order, 'currency' ) : get_woocommerce_currency(), @@ -124,11 +125,16 @@ function wcs_create_subscription( $args = array() ) { $args = wp_parse_args( $args, $default_args ); $subscription_data = array(); - // validate the start_date field + // Validate the date_created arg. + if ( ! is_string( $args['date_created'] ) || false === wcs_is_datetime_mysql_format( $args['date_created'] ) ) { + return new WP_Error( 'woocommerce_subscription_invalid_date_created_format', _x( 'Invalid created date. The date must be a string and of the format: "Y-m-d H:i:s".', 'Error message while creating a subscription', 'woocommerce-subscriptions' ) ); + } elseif ( wcs_date_to_time( $args['date_created'] ) > current_time( 'timestamp', true ) ) { + return new WP_Error( 'woocommerce_subscription_invalid_date_created', _x( 'Subscription created date must be before current day.', 'Error message while creating a subscription', 'woocommerce-subscriptions' ) ); + } + + // Validate the start_date arg. if ( ! is_string( $args['start_date'] ) || false === wcs_is_datetime_mysql_format( $args['start_date'] ) ) { return new WP_Error( 'woocommerce_subscription_invalid_start_date_format', _x( 'Invalid date. The date must be a string and of the format: "Y-m-d H:i:s".', 'Error message while creating a subscription', 'woocommerce-subscriptions' ) ); - } else if ( wcs_date_to_time( $args['start_date'] ) > current_time( 'timestamp', true ) ) { - return new WP_Error( 'woocommerce_subscription_invalid_start_date', _x( 'Subscription start date must be before current day.', 'Error message while creating a subscription', 'woocommerce-subscriptions' ) ); } // check customer id is set @@ -149,14 +155,14 @@ function wcs_create_subscription( $args = array() ) { $subscription_data['post_type'] = 'shop_subscription'; $subscription_data['post_status'] = 'wc-' . apply_filters( 'woocommerce_default_subscription_status', 'pending' ); $subscription_data['ping_status'] = 'closed'; - $subscription_data['post_author'] = 1; + $subscription_data['post_author'] = WC_Subscriptions::is_woocommerce_pre( '3.5' ) ? 1 : $args['customer_id']; $subscription_data['post_password'] = uniqid( 'order_' ); // translators: Order date parsed by strftime $post_title_date = strftime( _x( '%b %d, %Y @ %I:%M %p', 'Used in subscription post title. "Subscription renewal order - "', 'woocommerce-subscriptions' ) ); // translators: placeholder is order date parsed by strftime $subscription_data['post_title'] = sprintf( _x( 'Subscription – %s', 'The post title for the new subscription', 'woocommerce-subscriptions' ), $post_title_date ); - $subscription_data['post_date_gmt'] = $args['start_date']; - $subscription_data['post_date'] = get_date_from_gmt( $args['start_date'] ); + $subscription_data['post_date_gmt'] = $args['date_created']; + $subscription_data['post_date'] = get_date_from_gmt( $args['date_created'] ); if ( $args['order_id'] > 0 ) { $subscription_data['post_parent'] = absint( $args['order_id'] ); @@ -193,6 +199,8 @@ function wcs_create_subscription( $args = array() ) { update_post_meta( $subscription_id, '_customer_user', $args['customer_id'] ); update_post_meta( $subscription_id, '_order_version', $args['order_version'] ); + update_post_meta( $subscription_id, '_schedule_start', $args['start_date'] ); + /** * Filter the newly created subscription object. * @@ -363,10 +371,7 @@ function wcs_normalise_date_type_key( $date_type_key, $display_deprecated_notice $deprecated_notice = ''; - if ( 'start' === $date_type_key ) { - $deprecated_notice = 'The "start" date type parameter has been deprecated to align date types with improvements to date APIs in WooCommerce 3.0, specifically the introduction of a new "date_created" API. Use "date_created"'; - $date_type_key = 'date_created'; - } elseif ( 'last_payment' === $date_type_key ) { + if ( 'last_payment' === $date_type_key ) { $deprecated_notice = 'The "last_payment" date type parameter has been deprecated due to ambiguity (it actually returns the date created for the last order) and to align date types with improvements to date APIs in WooCommerce 3.0, specifically the introduction of a new "date_paid" API. Use "last_order_date_created" or "last_order_date_paid"'; // For backward compatibility we have to use the date created here not the 'date_paid', see: https://github.com/Prospress/woocommerce-subscriptions/issues/1943 $date_type_key = 'last_order_date_created'; @@ -775,3 +780,43 @@ function wcs_subscription_search( $term ) { return $subscription_ids; } + +/** + * Set payment method meta data for a subscription or order. + * + * @since 2.4.3 + * @param WC_Subscription|WC_Order $subscription The subscription or order to set the post payment meta on. + * @param array $payment_meta Associated array of the form: $database_table => array( 'meta_key' => array( 'value' => '' ) ) + * @throws InvalidArgumentException + */ +function wcs_set_payment_meta( $subscription, $payment_meta ) { + if ( ! is_array( $payment_meta ) ) { + throw new InvalidArgumentException( __( 'Payment method meta must be an array.', 'woocommerce-subscriptions' ) ); + } + + foreach ( $payment_meta as $meta_table => $meta ) { + foreach ( $meta as $meta_key => $meta_data ) { + if ( isset( $meta_data['value'] ) ) { + switch ( $meta_table ) { + case 'user_meta': + case 'usermeta': + update_user_meta( $subscription->get_user_id(), $meta_key, $meta_data['value'] ); + break; + case 'post_meta': + case 'postmeta': + if ( is_callable( array( $subscription, 'update_meta_data' ) ) ) { + $subscription->update_meta_data( $meta_key, $meta_data['value'] ); + } else { + update_post_meta( wcs_get_objects_property( $subscription, 'id' ), $meta_key, $meta_data['value'] ); + } + break; + case 'options': + update_option( $meta_key, $meta_data['value'] ); + break; + default: + do_action( 'wcs_save_other_payment_meta', $subscription, $meta_table, $meta_key, $meta_data['value'] ); + } + } + } + } +} diff --git a/woo-includes/class-wc-dependencies.php b/woo-includes/class-wc-dependencies.php old mode 100755 new mode 100644 diff --git a/woo-includes/woo-functions.php b/woo-includes/woo-functions.php old mode 100755 new mode 100644 index 744be3b..f92bdb1 --- a/woo-includes/woo-functions.php +++ b/woo-includes/woo-functions.php @@ -2,8 +2,9 @@ /** * Functions used by plugins */ -if ( ! class_exists( 'WC_Dependencies' ) ) - require_once 'class-wc-dependencies.php'; +if ( ! class_exists( 'WC_Dependencies' ) ) { + require_once( dirname( __FILE__ ) . '/class-wc-dependencies.php' ); +} /** * WC Detection @@ -94,4 +95,4 @@ if ( ! class_exists( 'WooThemes_Updater' ) && ! function_exists( 'woothemes_upda */ if ( ! class_exists( 'WooThemes_Plugin_Updater' ) ) { class WooThemes_Plugin_Updater { function init() {} } -} \ No newline at end of file +} diff --git a/woocommerce-subscriptions.php b/woocommerce-subscriptions.php old mode 100755 new mode 100644 index 5deb59f..046430d --- a/woocommerce-subscriptions.php +++ b/woocommerce-subscriptions.php @@ -1,14 +1,14 @@ register(); -require_once( 'includes/class-wc-subscriptions-product.php' ); - -require_once( 'includes/admin/class-wc-subscriptions-admin.php' ); - -require_once( 'includes/class-wc-subscriptions-manager.php' ); - -require_once( 'includes/class-wc-subscriptions-cart.php' ); - -require_once( 'includes/class-wc-subscriptions-order.php' ); - -require_once( 'includes/class-wc-subscriptions-renewal-order.php' ); - -require_once( 'includes/class-wc-subscriptions-checkout.php' ); - -require_once( 'includes/class-wc-subscriptions-email.php' ); - -require_once( 'includes/class-wc-subscriptions-addresses.php' ); - -require_once( 'includes/class-wc-subscriptions-change-payment-gateway.php' ); - -require_once( 'includes/gateways/class-wc-subscriptions-payment-gateways.php' ); - -require_once( 'includes/gateways/paypal/class-wcs-paypal.php' ); - -require_once( 'includes/class-wc-subscriptions-switcher.php' ); - -require_once( 'includes/class-wc-subscriptions-synchroniser.php' ); - -require_once( 'includes/upgrades/class-wc-subscriptions-upgrader.php' ); - -require_once( 'includes/upgrades/class-wcs-upgrade-logger.php' ); - -require_once( 'includes/libraries/action-scheduler/action-scheduler.php' ); - -require_once( 'includes/abstracts/abstract-wcs-scheduler.php' ); - -require_once( 'includes/class-wcs-action-scheduler.php' ); - -require_once( 'includes/abstracts/abstract-wcs-cache-manager.php' ); - -require_once( 'includes/class-wcs-cached-data-manager.php' ); - -require_once( 'includes/class-wcs-post-meta-cache-manager.php' ); - -require_once( 'includes/class-wcs-post-meta-cache-manager-many-to-one.php' ); - -require_once( 'includes/class-wcs-cart-renewal.php' ); - -require_once( 'includes/class-wcs-cart-resubscribe.php' ); - -require_once( 'includes/class-wcs-cart-initial-payment.php' ); - -require_once( 'includes/class-wcs-download-handler.php' ); - -require_once( 'includes/class-wcs-retry-manager.php' ); - -require_once( 'includes/class-wcs-cart-switch.php' ); - -require_once( 'includes/class-wcs-limiter.php' ); - -require_once( 'includes/interfaces/interface-wcs-cache-updater.php' ); - -require_once( 'includes/abstracts/abstract-wcs-related-order-store.php' ); - -require_once( 'includes/data-stores/class-wcs-related-order-store-cpt.php' ); - -require_once( 'includes/data-stores/class-wcs-related-order-store-cached-cpt.php' ); - -require_once( 'includes/abstracts/abstract-wcs-customer-store.php' ); - -require_once( 'includes/data-stores/class-wcs-customer-store-cpt.php' ); - -require_once( 'includes/data-stores/class-wcs-customer-store-cached-cpt.php' ); - -require_once( 'includes/legacy/class-wcs-array-property-post-meta-black-magic.php' ); - -require_once( 'includes/class-wcs-failed-scheduled-action-manager.php' ); - -require_once( dirname( __FILE__ ) . '/includes/admin/class-wcs-admin-system-status.php' ); - -require_once( 'includes/abstracts/abstract-wcs-debug-tool.php' ); - -require_once( 'includes/abstracts/abstract-wcs-background-updater.php' ); - -require_once( 'includes/admin/debug-tools/class-wcs-debug-tool-factory.php' ); - -require_once( 'includes/admin/class-wcs-admin-notice.php' ); - -require_once( 'includes/upgrades/class-wcs-upgrade-notice-manager.php' ); - -require_once( 'includes/abstracts/abstract-wcs-background-upgrader.php' ); - -require_once( 'includes/class-wcs-staging.php' ); +// Load libraries manually. +require_once( dirname( __FILE__ ) . '/includes/libraries/action-scheduler/action-scheduler.php' ); +// Initialize our classes. +WC_Subscriptions_Coupon::init(); +WC_Subscriptions_Product::init(); +WC_Subscriptions_Admin::init(); +WC_Subscriptions_Manager::init(); +WC_Subscriptions_Cart::init(); +WC_Subscriptions_Order::init(); +WC_Subscriptions_Renewal_Order::init(); +WC_Subscriptions_Checkout::init(); +WC_Subscriptions_Email::init(); +WC_Subscriptions_Addresses::init(); +WC_Subscriptions_Change_Payment_Gateway::init(); +WC_Subscriptions_Payment_Gateways::init(); +WCS_PayPal_Standard_Change_Payment_Method::init(); +WC_Subscriptions_Switcher::init(); +WCS_Upgrade_Logger::init(); +new WCS_Cart_Renewal(); +new WCS_Cart_Resubscribe(); +new WCS_Cart_Initial_Payment(); +WCS_Download_Handler::init(); +WCS_Retry_Manager::init(); +new WCS_Cart_Switch(); +WCS_Limiter::init(); WCS_Admin_System_Status::init(); WCS_Upgrade_Notice_Manager::init(); WCS_Staging::init(); +// Some classes run init on a particular hook. +add_action( 'init', array( 'WC_Subscriptions_Synchroniser', 'init' ) ); +add_action( 'after_setup_theme', array( 'WC_Subscriptions_Upgrader', 'init' ), 11 ); +add_action( 'init', array( 'WC_PayPal_Standard_Subscriptions', 'init' ), 11 ); + /** * The main subscriptions class. * @@ -170,9 +112,9 @@ class WC_Subscriptions { public static $plugin_file = __FILE__; - public static $version = '2.3.7'; + public static $version = '2.4.5'; - public static $wc_minimum_supported_version = '2.6'; + public static $wc_minimum_supported_version = '3.0'; private static $total_subscription_count = null; @@ -181,12 +123,18 @@ class WC_Subscriptions { /** @var WCS_Cache_Manager */ public static $cache; + /** @var WCS_Autoloader */ + protected static $autoloader; + /** * Set up the class, including it's hooks & filters, when the file is loaded. * * @since 1.0 - **/ - public static function init() { + * + * @param WCS_Autoloader $autoloader Autoloader instance. + */ + public static function init( $autoloader = null ) { + self::$autoloader = $autoloader ? $autoloader : new WCS_Autoloader( dirname( __FILE__ ) ); // Register our custom subscription order type after WC_Post_types::register_post_types() add_action( 'init', __CLASS__ . '::register_order_types', 6 ); @@ -222,7 +170,7 @@ class WC_Subscriptions { add_action( 'plugins_loaded', __CLASS__ . '::load_dependant_classes' ); // Attach hooks which depend on WooCommerce constants - add_action( 'plugins_loaded', __CLASS__ . '::attach_dependant_hooks' ); + add_action( 'plugins_loaded', array( __CLASS__, 'attach_dependant_hooks' ) ); // Make sure the related order data store instance is loaded and initialised so that cache management will function add_action( 'plugins_loaded', 'WCS_Related_Order_Store::instance' ); @@ -414,6 +362,8 @@ class WC_Subscriptions { if ( is_cart() || is_checkout() ) { wp_enqueue_script( 'wcs-cart', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/js/frontend/wcs-cart.js', $dependencies, WC_Subscriptions::$version, true ); + } elseif ( is_product() ) { + wp_enqueue_script( 'wcs-single-product', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/js/frontend/single-product.js', $dependencies, WC_Subscriptions::$version, true ); } } @@ -510,7 +460,7 @@ class WC_Subscriptions { if ( $is_subscription && 'yes' != get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) { // Generate a cart item key from variation and cart item data - which may be added by other plugins - $cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', array(), $product_id, $variation_id ); + $cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', array(), $product_id, $variation_id, $quantity ); $cart_item_id = WC()->cart->generate_cart_id( $product_id, $variation_id, $variations, $cart_item_data ); $product = wc_get_product( $product_id ); @@ -726,7 +676,7 @@ class WC_Subscriptions { } if ( $admin_notice_content ) { - require_once( 'includes/admin/class-wcs-admin-notice.php' ); + require_once( dirname( __FILE__ ) . '/includes/admin/class-wcs-admin-notice.php' ); $notice = new WCS_Admin_Notice( 'error' ); $notice->set_simple_content( $admin_notice_content ); $notice->display(); @@ -813,84 +763,36 @@ class WC_Subscriptions { * @since 1.2.4 */ public static function load_dependant_classes() { - - require_once( 'includes/class-wc-subscription.php' ); - - require_once( 'includes/class-wc-product-subscription.php' ); - - require_once( 'includes/class-wc-product-subscription-variation.php' ); - - require_once( 'includes/class-wc-product-variable-subscription.php' ); - - require_once( 'includes/admin/class-wcs-admin-post-types.php' ); - - require_once( 'includes/admin/class-wcs-admin-meta-boxes.php' ); - - require_once( 'includes/admin/class-wcs-admin-reports.php' ); - - require_once( 'includes/admin/reports/class-wcs-report-cache-manager.php' ); - - require_once( 'includes/admin/meta-boxes/class-wcs-meta-box-related-orders.php' ); - - require_once( 'includes/admin/meta-boxes/class-wcs-meta-box-subscription-data.php' ); - - require_once( 'includes/admin/meta-boxes/class-wcs-meta-box-subscription-schedule.php' ); - - require_once( 'includes/class-wcs-change-payment-method-admin.php' ); - - require_once( 'includes/class-wcs-webhooks.php' ); - - require_once( 'includes/class-wcs-auth.php' ); - - require_once( 'includes/class-wcs-api.php' ); - - require_once( 'includes/class-wcs-template-loader.php' ); - - require_once( 'includes/class-wcs-query.php' ); - - require_once( 'includes/class-wcs-remove-item.php' ); - - require_once( 'includes/class-wcs-user-change-status-handler.php' ); - - require_once( 'includes/class-wcs-my-account-payment-methods.php' ); + new WCS_Admin_Post_Types(); + new WCS_Admin_Meta_Boxes(); + new WCS_Admin_Reports(); + new WCS_Report_Cache_Manager(); + WCS_Webhooks::init(); + new WCS_Auth(); + WCS_API::init(); + WCS_Template_Loader::init(); + new WCS_Query(); + WCS_Remove_Item::init(); + WCS_User_Change_Status_Handler::init(); + WCS_My_Account_Payment_Methods::init(); if ( self::is_woocommerce_pre( '3.0' ) ) { - - require_once( 'includes/legacy/class-wc-subscription-legacy.php' ); - - require_once( 'includes/legacy/class-wcs-product-legacy.php' ); - - require_once( 'includes/legacy/class-wc-product-subscription-legacy.php' ); - - require_once( 'includes/legacy/class-wc-product-subscription-variation-legacy.php' ); - - require_once( 'includes/legacy/class-wc-product-variable-subscription-legacy.php' ); + WCS_Product_Legacy::init(); // Load WC_DateTime when it doesn't exist yet so we can use it for datetime handling consistently with WC 3.0+ if ( ! class_exists( 'WC_DateTime' ) ) { - require_once( 'includes/libraries/class-wc-datetime.php' ); + require_once( dirname( __FILE__ ) . '/includes/libraries/class-wc-datetime.php' ); } } else { - require_once( 'includes/class-wc-order-item-pending-switch.php' ); - require_once( 'includes/data-stores/class-wcs-subscription-data-store-cpt.php' ); - require_once( 'includes/deprecated/class-wcs-deprecated-filter-hooks.php' ); - require_once( 'includes/data-stores/class-wcs-product-variable-data-store-cpt.php' ); + new WCS_Deprecated_Filter_Hooks(); } // Provide a hook to enable running deprecation handling for stores that might want to check for deprecated code if ( apply_filters( 'woocommerce_subscriptions_load_deprecation_handlers', false ) ) { - - require_once( 'includes/abstracts/abstract-wcs-hook-deprecator.php' ); - - require_once( 'includes/abstracts/abstract-wcs-dynamic-hook-deprecator.php' ); - - require_once( 'includes/deprecated/class-wcs-action-deprecator.php' ); - - require_once( 'includes/deprecated/class-wcs-filter-deprecator.php' ); - - require_once( 'includes/deprecated/class-wcs-dynamic-action-deprecator.php' ); - - require_once( 'includes/deprecated/class-wcs-dynamic-filter-deprecator.php' ); + new WCS_Action_Deprecator(); + new WCS_Filter_Deprecator(); + new WCS_Dynamic_Action_Deprecator(); + new WCS_Dynamic_Filter_Deprecator(); } if ( class_exists( 'WCS_Early_Renewal' ) ) { @@ -906,13 +808,9 @@ class WC_Subscriptions { $notice->display(); } else { - require_once( dirname( __FILE__ ) . '/includes/early-renewal/class-wcs-early-renewal-manager.php' ); WCS_Early_Renewal_Manager::init(); - if ( WCS_Early_Renewal_Manager::is_early_renewal_enabled() ) { - require_once( dirname( __FILE__ ) . '/includes/early-renewal/class-wcs-cart-early-renewal.php' ); require_once( dirname( __FILE__ ) . '/includes/early-renewal/wcs-early-renewal-functions.php' ); - new WCS_Cart_Early_Renewal(); } } @@ -921,9 +819,6 @@ class WC_Subscriptions { $failed_scheduled_action_manager->init(); if ( class_exists( 'WC_Abstract_Privacy' ) ) { - require_once( 'includes/privacy/class-wcs-privacy.php' ); - require_once( 'includes/privacy/class-wcs-privacy-background-updater.php' ); - new WCS_Privacy(); } } @@ -942,6 +837,9 @@ class WC_Subscriptions { // Display Subscriptions on a User's account page add_action( 'woocommerce_before_my_account', __CLASS__ . '::get_my_subscriptions_template' ); } + + // Ensure the autoloader knows which API to use. + self::$autoloader->use_legacy_api( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ); } /** @@ -967,39 +865,36 @@ class WC_Subscriptions { wp_safe_redirect( remove_query_arg( array( 'wc_subscription_duplicate_site', '_wcsnonce' ) ) ); - } elseif ( self::get_current_sites_duplicate_lock() !== get_option( 'wcs_ignore_duplicate_siteurl_notice' ) ) { ?> + } elseif ( self::get_current_sites_duplicate_lock() !== get_option( 'wcs_ignore_duplicate_siteurl_notice' ) ) { + $notice = new WCS_Admin_Notice( 'error' ); + $notice->set_simple_content( + sprintf( + // translators: 1$-2$: opening and closing tags. 3$-4$: opening and closing link tags for learn more. Leads to duplicate site article on docs. 5$-6$: Opening and closing link to production URL. 7$: Production URL . + esc_html__( 'It looks like this site has moved or is a duplicate site. %1$sWooCommerce Subscriptions%2$s has disabled automatic payments and subscription related emails on this site to prevent duplicate payments from a staging or test environment. %1$sWooCommerce Subscriptions%2$s considers %5$s%7$s%6$s to be the site\'s URL. %3$sLearn more »%4$s.', 'woocommerce-subscriptions' ), + '', '', + '', '', + '', '', + esc_url( self::get_site_url_from_source( 'subscriptions_install' ) ) + ) + ); + $notice->set_actions( array( + array( + 'name' => __( 'Quit nagging me (but don\'t enable automatic payments)', 'woocommerce-subscriptions' ), + 'url' => wp_nonce_url( add_query_arg( 'wc_subscription_duplicate_site', 'ignore' ), 'wcs_duplicate_site', '_wcsnonce' ), + 'class' => 'button button-primary', + ), + array( + 'name' => __( 'Enable automatic payments', 'woocommerce-subscriptions' ), + 'url' => wp_nonce_url( add_query_arg( 'wc_subscription_duplicate_site', 'update' ), 'wcs_duplicate_site', '_wcsnonce' ), + 'class' => 'button', + ), + ) ); -
    -

    tags, 3$-4$: opening and closing link tags. Leads to duplicate site article on docs - printf( esc_html__( 'It looks like this site has moved or is a duplicate site. %1$sWooCommerce Subscriptions%2$s has disabled automatic payments and subscription related emails on this site to prevent duplicate payments from a staging or test environment. %3$sLearn more »%4$s.', 'woocommerce-subscriptions' ), '', '', '', '' ); ?>

    -
    - - -
    -
    - display(); } } } - /** - * Get's a WC_Product using the new core WC @see wc_get_product() function if available, otherwise - * instantiating an instance of the WC_Product class. - * - * @since 1.2.4 - */ - public static function get_product( $product_id ) { - - if ( function_exists( 'wc_get_product' ) ) { - $product = wc_get_product( $product_id ); - } else { - $product = new WC_Product( $product_id ); // Shouldn't matter if product is variation as all we need is the product_type - } - - return $product; - } - /** * A general purpose function for grabbing an array of subscriptions in form of 'subscription_key' => 'subscription_details'. * @@ -1202,28 +1097,30 @@ class WC_Subscriptions { } /** - * Creates a URL based on the current site's URL that can be used to prevent duplicate payments from staging sites. + * Creates a URL to prevent duplicate payments from staging sites. * - * The URL can not simply be the site URL, e.g. http://example.com, because WP Engine replaces all instances of the site URL in the database - * when creating a staging site. As a result, we obfuscate the URL by inserting '_[wc_subscriptions_siteurl]_' into the middle of it. + * The URL can not simply be the site URL, e.g. http://example.com, because WP Engine replaces all + * instances of the site URL in the database when creating a staging site. As a result, we obfuscate + * the URL by inserting '_[wc_subscriptions_siteurl]_' into the middle of it. * - * Why not just use a hash? Because keeping the URL in the value allows for viewing and editing the URL directly in the database. + * We don't use a hash because keeping the URL in the value allows for viewing and editing the URL + * directly in the database. * - * @param mixed $links * @since 1.4.2 + * @return string The duplicate lock URL. */ public static function get_current_sites_duplicate_lock() { - $site_url = self::get_site_url_from_source( 'current_wp_site' ); + $scheme = parse_url( $site_url, PHP_URL_SCHEME ) . '://'; + $site_url = str_replace( $scheme, '', $site_url ); - return substr_replace( $site_url, '_[wc_subscriptions_siteurl]_', strlen( $site_url ) / 2, 0 ); + return $scheme . substr_replace( $site_url, '_[wc_subscriptions_siteurl]_', strlen( $site_url ) / 2, 0 ); } /** * Sets a flag in the database to record the site's url. This then checked to determine if we are on a duplicate * site or the original/main site, uses @see self::get_current_sites_duplicate_lock(); * - * @param mixed $links * @since 1.4.2 */ public static function set_duplicate_site_url_lock() { @@ -1300,6 +1197,18 @@ class WC_Subscriptions { /* Deprecated Functions */ + /** + * Gets a WC_Product using the new core WC @see wc_get_product() function if available, otherwise + * instantiating an instance of the WC_Product class. + * + * @since 1.2.4 + * @deprecated 2.4.0 + */ + public static function get_product( $product_id ) { + _deprecated_function( __METHOD__, '2.4.0', 'wc_get_product()' ); + return wc_get_product( $product_id ); + } + /** * Add WooCommerce error or success notice regardless of the version of WooCommerce running. * @@ -1439,4 +1348,4 @@ class WC_Subscriptions { } } -WC_Subscriptions::init(); +WC_Subscriptions::init( $wcs_autoloader );