This commit is contained in:
Prospress Inc
2019-04-05 09:35:18 +02:00
committed by Remco Tolsma
parent 99a693a46f
commit e38fdb9d42
118 changed files with 4680 additions and 2532 deletions

View File

@@ -565,6 +565,40 @@ table.wp-list-table .subscription_renewal_order:after {
} }
} }
/* WooCommerce Payment Methods Settings page */
.payment-method-features-info {
font-size: 1.4em;
display: inline-block;
text-indent: -9999px;
position: relative;
height: 1em;
width: 1em
}
.payment-method-features-info::before {
font-family: WooCommerce;
speak: none;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
margin: 0;
text-indent: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
content: "\e018";
color: #a46497;
}
table.wc_gateways .renewals .tips{
margin: 0 0.2em;
display: inline-block;
}
/* Hide irrelevant sections on Edit Subscription screen */ /* Hide irrelevant sections on Edit Subscription screen */
body.post-type-shop_subscription .order_actions #actions optgroup[label='Resend order emails'], body.post-type-shop_subscription .order_actions #actions optgroup[label='Resend order emails'],
body.post-type-shop_subscription .add-items .description.tips, body.post-type-shop_subscription .add-items .description.tips,

View File

@@ -11,6 +11,31 @@
color: #f29ec4; color: #f29ec4;
} }
#woocommerce_dashboard_status .wc_status_list li.cancel-count {
width: 100%;
}
#woocommerce_dashboard_status .wc_status_list li.cancel-count a:before {
content: "\e02c";
color: #aa0000;
}
#woocommerce_dashboard_status .wc_status_list li.signup-revenue a:before {
font-family: Dashicons;
content: '\f185';
color: #5da5da;
}
#woocommerce_dashboard_status .wc_status_list li.renewal-revenue a:before {
font-family: Dashicons;
content: '\f185';
color: #f29ec4;
}
#woocommerce_dashboard_status .wc_status_list li.signup-count { #woocommerce_dashboard_status .wc_status_list li.signup-count {
border-right: 1px solid #ececec; border-right: 1px solid #ececec;
} }
#woocommerce_dashboard_status .wc_status_list li.renewal-count {
border-right: 1px solid #ececec;
}

View File

@@ -1,6 +1,58 @@
.subscription_details .button { @media only screen and (max-width:768px) {
.subscription_details .button {
margin-bottom: 2px; margin-bottom: 2px;
width: 100%; width: 100%;
max-width: 200px; max-width: 200px;
text-align: center; text-align: center;
}
}
.subscription-auto-renew-toggle {
margin-left: 5px;
margin-bottom: 2px;
position: relative;
top: 4px;
}
.subscription-auto-renew-toggle__i {
height: 20px;
width: 32px;
border: 2px solid #00BA8A;
background-color: #00BA8A;
display: inline-block;
text-indent: -9999px;
border-radius: 10em;
position: relative;
margin-top: -1px;
vertical-align: text-top;
}
.subscription-auto-renew-toggle__i:before {
content: "";
display: block;
width: 16px;
height: 16px;
background: #fff;
position: absolute;
top: 0;
right: 0;
border-radius: 100%;
}
.subscription-auto-renew-toggle--off .subscription-auto-renew-toggle__i {
border-color: #999;
background-color: #999;
}
.subscription-auto-renew-toggle--off .subscription-auto-renew-toggle__i:before {
right: auto;
left: 0;
}
.subscription-auto-renew-toggle--loading .subscription-auto-renew-toggle__i {
opacity: 0.5;
}
.subscription-auto-renew-toggle--hidden {
display: none;
} }

View File

@@ -766,4 +766,43 @@ jQuery(document).ready(function($){
}; };
wcs_prevent_product_type_change.init(); wcs_prevent_product_type_change.init();
/*
* Handles enabling and disabling PayPal Standard for Subscriptions.
*/
var wcs_paypal_standard_settings = {
init: function() {
if ( 0 === $( '#woocommerce_paypal_enabled' ).length ) {
return;
}
$( '#woocommerce_paypal_enabled' ).on( 'change', this.paypal_enabled_change );
$( '#woocommerce_paypal_enabled_for_subscriptions' ).on( 'change', this.paypal_for_subscriptions_enabled );
this.paypal_enabled_change();
},
/**
* Show and hide the enable PayPal for Subscriptions checkbox when PayPal is enabled or disabled.
*/
paypal_enabled_change: function() {
var $enabled_for_subscriptions_element = $( '#woocommerce_paypal_enabled_for_subscriptions' ).closest( 'tr' );
if ( $( '#woocommerce_paypal_enabled' ).is( ':checked' ) ) {
$enabled_for_subscriptions_element.show();
} else {
$enabled_for_subscriptions_element.hide();
}
},
/**
* Display a confirm dialog when PayPal for Subscriptions is enabled (checked).
*/
paypal_for_subscriptions_enabled: function() {
if ( $( this ).is( ':checked' ) && ! confirm( WCSubscriptions.enablePayPalWarning ) ) {
$( this ).removeAttr( 'checked' );
}
}
};
wcs_paypal_standard_settings.init();
}); });

View File

@@ -0,0 +1,107 @@
jQuery( document ).ready( function( $ ) {
var $toggleContainer = $( '.wcs-auto-renew-toggle' );
var $toggle = $( '.subscription-auto-renew-toggle', $toggleContainer );
var $icon = $toggle.find( 'i' );
var txtColor = null;
var $paymentMethod = $( '.subscription-payment-method' );
function getTxtColor() {
if ( !txtColor && ( $icon && $icon.length ) ) {
txtColor = getComputedStyle( $icon[0] ).color;
}
return txtColor;
}
function maybeApplyColor() {
if ( $toggle.hasClass( 'subscription-auto-renew-toggle--on' ) && $icon.length ) {
$icon[0].style.backgroundColor = getTxtColor();
$icon[0].style.borderColor = getTxtColor();
} else if( $icon.length ) {
$icon[0].style.backgroundColor = null;
$icon[0].style.borderColor = null;
}
}
function displayToggle() {
$toggle.removeClass( 'subscription-auto-renew-toggle--hidden' );
}
function onToggle( e ) {
e.preventDefault();
// Remove focus from the toggle element.
$toggle.blur();
var ajaxHandler = function( action ) {
var data = {
subscription_id: WCSViewSubscription.subscription_id,
action: action,
security: WCSViewSubscription.auto_renew_nonce,
};
// While we're waiting for an AJAX response, block the toggle element to prevent spamming the server.
blockToggle();
$.ajax({
url: WCSViewSubscription.ajax_url,
data: data,
type: 'POST',
success: function( result ) {
if ( result.payment_method ) {
$paymentMethod.fadeOut( function() {
$paymentMethod.html( result.payment_method ).fadeIn();
});
}
},
error: function( jqxhr, status, exception ) {
alert( 'Exception:', exception );
},
complete: unblockToggle
});
};
// Enable auto-renew
if ( $toggle.hasClass( 'subscription-auto-renew-toggle--off' ) ) {
// if payment method already exists just turn automatic renewals on.
if ( WCSViewSubscription.has_payment_gateway ) {
ajaxHandler( 'wcs_enable_auto_renew' );
displayToggleOn();
} else if ( window.confirm( WCSViewSubscription.add_payment_method_msg ) ) { // else add payment method
window.location.href = WCSViewSubscription.add_payment_method_url;
}
} else { // Disable auto-renew
ajaxHandler( 'wcs_disable_auto_renew' );
displayToggleOff();
}
maybeApplyColor();
}
function displayToggleOn() {
$icon.removeClass( 'fa-toggle-off' ).addClass( 'fa-toggle-on' );
$toggle.removeClass( 'subscription-auto-renew-toggle--off' ).addClass( 'subscription-auto-renew-toggle--on' );
}
function displayToggleOff() {
$icon.removeClass( 'fa-toggle-on' ).addClass( 'fa-toggle-off' );
$toggle.removeClass( 'subscription-auto-renew-toggle--on' ).addClass( 'subscription-auto-renew-toggle--off' );
}
function blockToggle() {
$toggleContainer.block({
message: null,
overlayCSS: { opacity: 0.0 }
});
}
function unblockToggle() {
$toggleContainer.unblock();
}
$toggle.on( 'click', onToggle );
maybeApplyColor();
displayToggle();
});

View File

@@ -10,8 +10,16 @@ function hide_non_applicable_coupons() {
hide_non_applicable_coupons(); hide_non_applicable_coupons();
jQuery( document ).ready( function( $ ){ jQuery( document ).ready( function( $ ) {
$( document.body ).on( 'updated_cart_totals updated_checkout', function() { $( document.body ).on( 'updated_cart_totals updated_checkout', function() {
hide_non_applicable_coupons(); hide_non_applicable_coupons();
} ); } );
$( '.payment_methods [name="payment_method"]' ).click( function() {
if ( $( this ).hasClass( 'supports-payment-method-changes' ) ) {
$( '.update-all-subscriptions-payment-method-wrap' ).show();
} else {
$( '.update-all-subscriptions-payment-method-wrap' ).hide();
}
} );
} ); } );

View File

@@ -1,5 +1,77 @@
*** WooCommerce Subscriptions Changelog *** *** WooCommerce Subscriptions Changelog ***
2019.03.20 - version 2.5.3
* New: Update Action Scheduler to version 2.2.1. PR#3266
* Fix: Display the subscription status names (rather than the raw status) in the edit subscription related orders table. PR#3272
* Fix: Use the correct endpoint for redirecting to the only available subscription on My Account. PR#3273
* Fix: [WPML] Keep language request args in Subscription My Account endpoint URLs. PR#3068
* Fix: Save the sale price schedule dates in UTC time (like WooCommerce core). PR#3270
* Fix: Use site time when checking if payment date and trial end date are same. Fixes issues with synced products with a trial. PR#3264
* Fix: Update retry report queries to refer to new retry tables. PR#3237
* Fix: Do not display the PayPal Profile ID on the edit subscription screen when the subscription is Manual Renewal. PR#3259
* Fix: Exclude `pending-cancel` subscriptions from anonymisation. PR#3256
* Fix: Pass the user ID and gateway ID parameters in the right order. Fixes issues with bulk updating customer payment tokens. PR#3258
* Fix: Temporarily lock payment on parent orders paid with PayPal Standard. Prevents customers paying twice while waiting for PayPal to respond. PR#2974
* Fix: Remove HTML from strings passed through translation functions. PR#3067
* Fix: Prevent endpoint settings having the same value. PR#3211
* Fix: Remove `strtolower()` uses on translated text. Prevents lowercasing translated text which may have language specific capitalisation. PR#3251
* Fix: Prevent possible fatal errors while populating the checkout with customer order data. Refactors the wcs_get_objects_property function. PR#3225
* Fix: Removes use of absint for amounts/totals in reports. PR#3242
* Fix: Display correct placeholder values for 'subject' and 'heading' e-mail settings #3246
* Fix: Do not load the view-subscription scripts when subscription can't be viewed or doesn't exist. PR#3235
* Fix: [WC3.6] Use new helper function to generate cart hash. PR#3215
* Tweak: Use 'WC_Order_Item_Product::get_product' wherever possible in 'WC_Subscriptions_Synchroniser'. PR#3230
* Tweak: Clarify the meaning of certain count values in subscription reports tool tips. PR#3143
* Tweak: Fixes various typos. PR#3276
* Dev: Add $cart as a parameter for the woocommerce_cart_subscription_string_details hook. PR#3257
2019.02.21 - version 2.5.2
* Fix: Check if sale price is empty before returning it as the product price. Fixes compat. issues with Dynamic Pricing. PR#3213
* Fix: Copy subscription meta data to early renewal orders. PR#3221
* Fix: Add jquery-blockui as a dependency of the view-subscription JS file. PR#3227
* Fix: Validate the DOM object exists before applying styles. Fixes JS errors on the My Account View Subscription page. PR#3218
* Tweak: Escape line item names via wp_kses_post to allow safe html tags. PR#3224
* Dev: Filter the wcs_cart_totals_shipping_method_price_label function to allow third-parties to alter the free shipping label. PR#3228
* Dev: Add $variation parameter to 'woocommerce_subscriptions_product_needs_one_time_shipping' filter. PR#3197
2019.02.11 - version 2.5.1
* Fix: In report queries use WordPress prefixed table names. PR#3205
* Fix: Fix errors caused by calling WC_Gateway_Paypal::update_option() in early WooCommerce versions. PR#3208
* Tweak: Rename the 'uncancel' admin subscription action to 'reactivate'. PR#3209
* Enhancement: Add downloadable and virtual subscription options to the filter on the products admin page. PR#3199
2019.02.06 - version 2.5.0
* New: Add payment gateway feature support tooltips to the WooCommerce>Setting>Payments screen. PR#3013
* New: Add Subscriptions-related information to the WooCommerce Status Widget. PR#2980
* New: Add option to allow a $0 initial sign-up without payment method. PR#3015
* New: Update Action Scheduler to v2.2.0. PR#3187
* New: Allow customers to update all subscriptions' payment method. PRs#3064,#3080,#3041
* New: Display downloads in separate table on the customer's View Subscription page. PR#3128
* New: Add new option to filter Admin subscriptions table by manual subscriptions. PR#3144
* New: Add auto-renew toggle to Customer's View Subscription page. PR #3156
* New: Add option to disable PayPal Standard for subscription-related purchases. PR#3152
* New: Add change payment method link to the edit subscription screen. PR#3086
* New: Allow store managers and customers to uncancel a subscription while it's still in a pending-cancel status. PR#2993
* Fix: Prevent PHP warning in WC_Subscriptions_Cart::cart_needs_payment(). PR#3142
* Fix: Do not return non-order objects from WC_Subscription::get_related_orders( 'all' ). Prevents errors when the calling function expects an order but got false. PR#3140
* Fix: Don't display the subscriptions My Account menu item when endpoint is empty. PR#3146
* Fix: Fix rounding warnings with PayPal Reference Transactions. PR#3134
* Fix: Remove use of an invalid Exception object type. PR#3148
* Fix: Fallback to previous renewal when parent order is not define for PayPal Standard IPNs. PR#2855
* Fix: Update WC_Subscription::status_transition() method to be inline with WC_Order::status_transition(). PR#2777
* Fix: Skip non-orders objects when generating the related orders table to prevent fatal errors. PR#3161
* Fix: Redirect non-logged in users to login when handling requests to change a payment method. PR#3160
* Fix: Don't apply retry rules to manual renewal payment attempts. PR#3153
* Tweak: Revert subscription customer ID and post_author changes introduced for WC3.5. PR#3159
* Tweak: Update failed scheduled action notice content. PR#3170
* Tweak: Ignore all $0.01 PayPal Standard IPN transactions. PR#3178
* Tweak: Add installed plugin information to the log file during plugin upgrade. PR#3181
* Dev: Add WC_Subscription::get_change_payment_method_url() function. PR#3041
* Dev: Add a filter to allow custom relations to be supported by the related order store. PR#3089
* Dev: Update uses of woocommerce_order_item_meta_end to use add_action, remove_action - not filter APIs. #3163
* Dev: [WC 3.5.4] Subscription order key updates. PR#3171
* Dev: Deprecate WC_Subscriptions_Cart::pre_get_refreshed_fragments. PR#3173
2018.12.21 - version 2.4.7 2018.12.21 - version 2.4.7
* Fix: Fix an issue with the release date of 2.4.6. * Fix: Fix an issue with the release date of 2.4.6.

1252
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,11 +41,7 @@ abstract class WCS_Related_Order_Store {
* *
* @var array * @var array
*/ */
private static $relation_type_keys = array( private static $relation_type_keys = array();
'renewal' => true,
'switch' => true,
'resubscribe' => true,
);
/** /**
* Get the active related order data store. * Get the active related order data store.
@@ -59,6 +55,19 @@ abstract class WCS_Related_Order_Store {
wcs_doing_it_wrong( __METHOD__, 'This method was called before the "plugins_loaded" hook. It applies a filter to the related order data store instantiated. For that to work, it should first be called after all plugins are loaded.', '2.3.0' ); wcs_doing_it_wrong( __METHOD__, 'This method was called before the "plugins_loaded" hook. It applies a filter to the related order data store instantiated. For that to work, it should first be called after all plugins are loaded.', '2.3.0' );
} }
/**
* Allow third-parties to register their own custom order relationship types which should be handled by this store.
*
* @param array An array of order relationship types.
* @since 2.5.0
*/
foreach ( (array) apply_filters( 'wcs_additional_related_order_relation_types', array() ) as $relation_type ) {
self::$relation_types[] = $relation_type;
}
self::$relation_type_keys = array_fill_keys( self::$relation_types, true );
$class = apply_filters( 'wcs_related_order_store_class', 'WCS_Related_Order_Store_Cached_CPT' ); $class = apply_filters( 'wcs_related_order_store_class', 'WCS_Related_Order_Store_Cached_CPT' );
self::$instance = new $class(); self::$instance = new $class();
self::$instance->init(); self::$instance->init();

View File

@@ -59,6 +59,10 @@ class WC_Subscriptions_Admin {
// Add subscriptions to the product select box // Add subscriptions to the product select box
add_filter( 'product_type_selector', __CLASS__ . '::add_subscription_products_to_select' ); add_filter( 'product_type_selector', __CLASS__ . '::add_subscription_products_to_select' );
// Special handling of downloadable and virtual products on the WooCommerce > Products screen.
add_filter( 'product_type_selector', array( __CLASS__, 'add_downloadable_and_virtual_filters' ) );
add_filter( 'request', array( __CLASS__, 'modify_downloadable_and_virtual_product_queries' ), 11 );
// Add subscription pricing fields on edit product page // Add subscription pricing fields on edit product page
add_action( 'woocommerce_product_options_general_product_data', __CLASS__ . '::subscription_pricing_fields' ); add_action( 'woocommerce_product_options_general_product_data', __CLASS__ . '::subscription_pricing_fields' );
@@ -106,15 +110,17 @@ class WC_Subscriptions_Admin {
add_filter( 'posts_where', __CLASS__ . '::filter_orders' ); add_filter( 'posts_where', __CLASS__ . '::filter_orders' );
add_filter( 'posts_where', array( __CLASS__, 'filter_paid_subscription_orders_for_user' ) );
add_action( 'admin_notices', __CLASS__ . '::display_renewal_filter_notice' ); add_action( 'admin_notices', __CLASS__ . '::display_renewal_filter_notice' );
add_shortcode( 'subscriptions', __CLASS__ . '::do_subscriptions_shortcode' ); add_shortcode( 'subscriptions', __CLASS__ . '::do_subscriptions_shortcode' );
add_filter( 'set-screen-option', __CLASS__ . '::set_manage_subscriptions_screen_option', 10, 3 ); add_filter( 'set-screen-option', __CLASS__ . '::set_manage_subscriptions_screen_option', 10, 3 );
add_filter( 'woocommerce_payment_gateways_setting_columns', __CLASS__ . '::payment_gateways_rewewal_column' ); add_filter( 'woocommerce_payment_gateways_setting_columns', array( __CLASS__, 'payment_gateways_renewal_column' ) );
add_action( 'woocommerce_payment_gateways_setting_column_renewals', __CLASS__ . '::payment_gateways_rewewal_support' ); add_action( 'woocommerce_payment_gateways_setting_column_renewals', array( __CLASS__, 'payment_gateways_renewal_support' ) );
// Do not display formatted order total on the Edit Order administration screen // Do not display formatted order total on the Edit Order administration screen
add_filter( 'woocommerce_get_formatted_order_total', __CLASS__ . '::maybe_remove_formatted_order_total_filter', 0, 2 ); add_filter( 'woocommerce_get_formatted_order_total', __CLASS__ . '::maybe_remove_formatted_order_total_filter', 0, 2 );
@@ -188,6 +194,74 @@ class WC_Subscriptions_Admin {
return $product_types; return $product_types;
} }
/**
* Add options for downloadable and virtual subscription products to the product type selector on the WooCommerce products screen.
*
* @param array $product_types
* @return array
* @since 2.5.1
*/
public static function add_downloadable_and_virtual_filters( $product_types ) {
global $typenow;
if ( ! is_admin() || ! doing_action( 'restrict_manage_posts' ) || 'product' !== $typenow ) {
return $product_types;
}
$product_options = array_reverse(
array(
'downloadable_subscription' => ( is_rtl() ? '←' : '→' ) . ' ' . __( 'Downloadable', 'woocommerce-subscriptions' ),
'virtual_subscription' => ( is_rtl() ? '←' : '→' ) . ' ' . __( 'Virtual', 'woocommerce-subscriptions' ),
)
);
foreach ( $product_options as $key => $label ) {
$product_types = wcs_array_insert_after( 'subscription', $product_types, $key, $label );
}
return $product_types;
}
/**
* Modifies the main query on the WooCommerce products screen to correctly handle filtering by virtual and downloadable
* product types.
*
* @param array $query_vars
* @return array $query_vars
* @since 2.5.1
*/
public static function modify_downloadable_and_virtual_product_queries( $query_vars) {
global $pagenow, $typenow;
if ( ! is_admin() || 'edit.php' !== $pagenow || 'product' !== $typenow ) {
return $query_vars;
}
$current_product_type = isset( $_REQUEST['product_type'] ) ? wc_clean( wp_unslash( $_REQUEST['product_type'] ) ) : false;
if ( ! $current_product_type ) {
return $query_vars;
}
if ( in_array( $current_product_type, array( 'downloadable', 'virtual' ) ) && ! isset( $query_vars['tax_query'] ) ) {
// Do not include subscriptions when the default "Downloadable" or "Virtual" query for simple products is being executed.
$query_vars['tax_query'] = array(
array(
'taxonomy' => 'product_type',
'terms' => array( 'subscription' ),
'field' => 'slug',
'operator' => 'NOT IN',
),
);
} elseif ( in_array( $current_product_type, array( 'downloadable_subscription', 'virtual_subscription' ) ) ) {
// Limit query to subscription products when the "Downloadable" or "Virtual" choices under "Simple Subscription" are being used.
$query_vars['meta_value'] = 'yes';
$query_vars['meta_key'] = '_' . str_replace( '_subscription', '', $current_product_type );
$query_vars['product_type'] = 'subscription';
}
return $query_vars;
}
/** /**
* Output the subscription specific pricing fields on the "Edit Product" admin page. * Output the subscription specific pricing fields on the "Edit Product" admin page.
* *
@@ -247,7 +321,8 @@ class WC_Subscriptions_Admin {
// Sign-up Fee // Sign-up Fee
woocommerce_wp_text_input( array( woocommerce_wp_text_input( array(
'id' => '_subscription_sign_up_fee', 'id' => '_subscription_sign_up_fee',
'class' => 'wc_input_subscription_intial_price wc_input_price short', // Keep wc_input_subscription_intial_price for backward compatibility.
'class' => 'wc_input_subscription_intial_price wc_input_subscription_initial_price wc_input_price short',
// translators: %s is a currency symbol / code // translators: %s is a currency symbol / code
'label' => sprintf( __( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ), 'label' => sprintf( __( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ),
'placeholder' => _x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ), 'placeholder' => _x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ),
@@ -389,8 +464,11 @@ class WC_Subscriptions_Admin {
update_post_meta( $post_id, '_regular_price', $subscription_price ); update_post_meta( $post_id, '_regular_price', $subscription_price );
update_post_meta( $post_id, '_sale_price', $sale_price ); update_post_meta( $post_id, '_sale_price', $sale_price );
$date_from = ( isset( $_POST['_sale_price_dates_from'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_from'] ) : ''; $site_offset = get_option( 'gmt_offset' ) * 3600;
$date_to = ( isset( $_POST['_sale_price_dates_to'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_to'] ) : '';
// Save the timestamps in UTC time, the way WC does it.
$date_from = ( isset( $_POST['_sale_price_dates_from'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_from'] ) - $site_offset : '';
$date_to = ( isset( $_POST['_sale_price_dates_to'] ) ) ? wcs_date_to_time( $_POST['_sale_price_dates_to'] ) - $site_offset : '';
$now = gmdate( 'U' ); $now = gmdate( 'U' );
@@ -746,12 +824,12 @@ class WC_Subscriptions_Admin {
'productHasSubscriptions' => wcs_get_subscriptions_for_product( $post->ID ) ? 'yes' : 'no', 'productHasSubscriptions' => wcs_get_subscriptions_for_product( $post->ID ) ? 'yes' : 'no',
'productTypeWarning' => __( 'Product type can not be changed because this product is associated with active subscriptions', 'woocommerce-subscriptions' ), 'productTypeWarning' => __( 'Product type can not be changed because this product is associated with active subscriptions', 'woocommerce-subscriptions' ),
); );
} else if ( 'edit-shop_order' == $screen->id ) { } elseif ( 'edit-shop_order' == $screen->id ) {
$script_params = array( $script_params = array(
'bulkTrashWarning' => __( "You are about to trash one or more orders which contain a subscription.\n\nTrashing the orders will also trash the subscriptions purchased with these orders.", 'woocommerce-subscriptions' ), 'bulkTrashWarning' => __( "You are about to trash one or more orders which contain a subscription.\n\nTrashing the orders will also trash the subscriptions purchased with these orders.", 'woocommerce-subscriptions' ),
'trashWarning' => $trashing_subscription_order_warning, 'trashWarning' => $trashing_subscription_order_warning,
); );
} else if ( 'shop_order' == $screen->id ) { } elseif ( 'shop_order' == $screen->id ) {
$dependencies[] = $woocommerce_admin_script_handle; $dependencies[] = $woocommerce_admin_script_handle;
$dependencies[] = 'wc-admin-order-meta-boxes'; $dependencies[] = 'wc-admin-order-meta-boxes';
@@ -767,10 +845,14 @@ class WC_Subscriptions_Admin {
'EditOrderNonce' => wp_create_nonce( 'woocommerce-subscriptions' ), 'EditOrderNonce' => wp_create_nonce( 'woocommerce-subscriptions' ),
'postId' => $post->ID, 'postId' => $post->ID,
); );
} else if ( 'users' == $screen->id ) { } elseif ( 'users' == $screen->id ) {
$script_params = array( $script_params = array(
'deleteUserWarning' => __( "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\nDo you want to continue to delete this user and any associated subscriptions?", 'woocommerce-subscriptions' ), 'deleteUserWarning' => __( "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\nDo you want to continue to delete this user and any associated subscriptions?", 'woocommerce-subscriptions' ),
); );
} elseif ( 'woocommerce_page_wc-settings' === $screen->id ) {
$script_params = array(
'enablePayPalWarning' => __( 'PayPal Standard has a number of limitations and does not support all subscription features.', 'woocommerce-subscriptions' ) . "\n\n" . __( 'Because of this, it is not recommended as a payment method for Subscriptions unless it is the only available option for your country.', 'woocommerce-subscriptions' ),
);
} }
$script_params['ajaxLoaderImage'] = WC()->plugin_url() . '/assets/images/ajax-loader.gif'; $script_params['ajaxLoaderImage'] = WC()->plugin_url() . '/assets/images/ajax-loader.gif';
@@ -1179,6 +1261,15 @@ class WC_Subscriptions_Admin {
'desc_tip' => __( 'Allow a subscription product to be purchased with other products and subscriptions in the same transaction.', 'woocommerce-subscriptions' ), 'desc_tip' => __( 'Allow a subscription product to be purchased with other products and subscriptions in the same transaction.', 'woocommerce-subscriptions' ),
), ),
array(
'name' => __( '$0 Initial Checkout', 'woocommerce-subscriptions' ),
'desc' => __( 'Allow $0 initial checkout without a payment method.', 'woocommerce-subscriptions' ),
'id' => self::$option_prefix . '_zero_initial_payment_requires_payment',
'default' => 'no',
'type' => 'checkbox',
'desc_tip' => __( 'Allow a subscription product with a $0 initial payment to be purchased without providing a payment method. The customer will be required to provide a payment method at the end of the initial period to keep the subscription active.', 'woocommerce-subscriptions' ),
),
array( array(
'name' => __( 'Drip Downloadable Content', 'woocommerce-subscriptions' ), 'name' => __( 'Drip Downloadable Content', 'woocommerce-subscriptions' ),
'desc' => __( 'Enable dripping for downloadable content on subscription products.', 'woocommerce-subscriptions' ), 'desc' => __( 'Enable dripping for downloadable content on subscription products.', 'woocommerce-subscriptions' ),
@@ -1301,9 +1392,7 @@ class WC_Subscriptions_Admin {
public static function filter_orders( $where ) { public static function filter_orders( $where ) {
global $typenow, $wpdb; global $typenow, $wpdb;
if ( is_admin() && 'shop_order' == $typenow ) { if ( is_admin() && 'shop_order' === $typenow ) {
$related_orders = array();
if ( isset( $_GET['_subscription_related_orders'] ) && $_GET['_subscription_related_orders'] > 0 ) { if ( isset( $_GET['_subscription_related_orders'] ) && $_GET['_subscription_related_orders'] > 0 ) {
@@ -1325,6 +1414,45 @@ class WC_Subscriptions_Admin {
return $where; return $where;
} }
/**
* Filter the "Orders" list to show only paid subscription orders for a particular user
*
* @param string $where
* @return string
* @since 2.5.3
*/
public static function filter_paid_subscription_orders_for_user( $where ) {
global $typenow, $wpdb;
if ( ! is_admin() || 'shop_order' !== $typenow || ! isset( $_GET['_paid_subscription_orders_for_customer_user'] ) || 0 == $_GET['_paid_subscription_orders_for_customer_user'] ) {
return $where;
}
$user_id = $_GET['_paid_subscription_orders_for_customer_user'];
// Unset the GET arg so that it doesn't interfere with the query for user's subscriptions.
unset( $_GET['_paid_subscription_orders_for_customer_user'] );
$users_subscriptions = wcs_get_users_subscriptions( $user_id );
$users_subscription_orders = array();
foreach ( $users_subscriptions as $subscription ) {
$users_subscription_orders = array_merge( $users_subscription_orders, $subscription->get_related_orders( 'ids' ) );
}
if ( empty( $users_subscription_orders ) ) {
wcs_add_admin_notice( sprintf( __( 'We can\'t find a paid subscription order for this user.', 'woocommerce-subscriptions' ) ), 'error' );
$where .= " AND {$wpdb->posts}.ID = 0";
} else {
// Orders with paid status
$where .= sprintf( " AND {$wpdb->posts}.post_status IN ( 'wc-processing', 'wc-completed' )" );
$where .= sprintf( " AND {$wpdb->posts}.ID IN (%s)", implode( ',', array_unique( $users_subscription_orders ) ) );
}
return $where;
}
/** /**
* Display a notice indicating that the "Orders" list is filtered. * Display a notice indicating that the "Orders" list is filtered.
* @see self::filter_orders() * @see self::filter_orders()
@@ -1473,27 +1601,41 @@ class WC_Subscriptions_Admin {
/** /**
* Add a column to the Payment Gateway table to show whether the gateway supports automated renewals. * Add a column to the Payment Gateway table to show whether the gateway supports automated renewals.
* *
* @since 1.5 * @param array $header
* @return string *
* @since 2.5.3
* @return array
*/ */
public static function payment_gateways_rewewal_column( $header ) { public static function payment_gateways_renewal_column( $header ) {
$header_new = array_slice( $header, 0, count( $header ) - 1, true ) + array( 'renewals' => __( 'Automatic Recurring Payments', 'woocommerce-subscriptions' ) ) + // Ideally, we could add a link to the docs here, but the title is passed through esc_html()
$header_new = array_slice( $header, 0, count( $header ) - 1, true ) +
array( 'renewals' => __( 'Automatic Recurring Payments', 'woocommerce-subscriptions' ) ) + // Ideally, we could add a link to the docs here, but the title is passed through esc_html()
array_slice( $header, count( $header ) - 1, count( $header ) - ( count( $header ) - 1 ), true ); array_slice( $header, count( $header ) - 1, count( $header ) - ( count( $header ) - 1 ), true );
return $header_new; return $header_new;
} }
/**
* Add a column to the Payment Gateway table to show whether the gateway supports automated renewals.
*
* @since 1.5
* @deprecated 2.5.3
* @return string
*/
public static function payment_gateways_rewewal_column( $header ) {
wcs_deprecated_function( __METHOD__, '2.5.3', 'WC_Subscriptions_Admin::payment_gateways_renewal_column( $header )' );
return self::payment_gateways_renewal_column( $header );
}
/** /**
* Check whether the payment gateway passed in supports automated renewals or not. * Check whether the payment gateway passed in supports automated renewals or not.
* Automatically flag support for Paypal since it is included with subscriptions. * Automatically flag support for Paypal since it is included with subscriptions.
* Display in the Payment Gateway column. * Display in the Payment Gateway column.
* *
* @since 1.5 * @param WC_Payment_Gateway $gateway
*
* @since 2.5.3
*/ */
public static function payment_gateways_rewewal_support( $gateway ) { public static function payment_gateways_renewal_support( $gateway ) {
echo '<td class="renewals">'; echo '<td class="renewals">';
if ( ( is_array( $gateway->supports ) && in_array( 'subscriptions', $gateway->supports ) ) || $gateway->id == 'paypal' ) { if ( ( is_array( $gateway->supports ) && in_array( 'subscriptions', $gateway->supports ) ) || $gateway->id == 'paypal' ) {
$status_html = '<span class="status-enabled tips" data-tip="' . esc_attr__( 'Supports automatic renewal payments with the WooCommerce Subscriptions extension.', 'woocommerce-subscriptions' ) . '">' . esc_html__( 'Yes', 'woocommerce-subscriptions' ) . '</span>'; $status_html = '<span class="status-enabled tips" data-tip="' . esc_attr__( 'Supports automatic renewal payments with the WooCommerce Subscriptions extension.', 'woocommerce-subscriptions' ) . '">' . esc_html__( 'Yes', 'woocommerce-subscriptions' ) . '</span>';
@@ -1508,6 +1650,7 @@ class WC_Subscriptions_Admin {
* Automatic Renewal Payments Support Status HTML Filter. * Automatic Renewal Payments Support Status HTML Filter.
* *
* @since 2.0 * @since 2.0
*
* @param string $status_html * @param string $status_html
* @param \WC_Payment_Gateway $gateway * @param \WC_Payment_Gateway $gateway
*/ */
@@ -1516,6 +1659,20 @@ class WC_Subscriptions_Admin {
echo '</td>'; echo '</td>';
} }
/**
* Check whether the payment gateway passed in supports automated renewals or not.
* Automatically flag support for Paypal since it is included with subscriptions.
* Display in the Payment Gateway column.
*
* @since 1.5
* @deprecated 2.5.3
*/
public static function payment_gateways_rewewal_support( $gateway ) {
wcs_deprecated_function( __METHOD__, '2.5.3', 'WC_Subscriptions_Admin::payment_gateways_renewal_support( $gateway )' );
return self::payment_gateways_renewal_support( $gateway );
}
/** /**
* Do not display formatted order total on the Edit Order administration screen * Do not display formatted order total on the Edit Order administration screen
* *

View File

@@ -495,7 +495,7 @@ class WCS_Admin_Post_Types {
} }
} else { } else {
if ( 'pending-cancel' === $the_subscription->get_status() ) { if ( 'cancelled' === $status && 'pending-cancel' === $the_subscription->get_status() ) {
$label = __( 'Cancel Now', 'woocommerce-subscriptions' ); $label = __( 'Cancel Now', 'woocommerce-subscriptions' );
} }
@@ -772,20 +772,29 @@ class WCS_Admin_Post_Types {
} }
if ( ! empty( $_GET['_payment_method'] ) ) { if ( ! empty( $_GET['_payment_method'] ) ) {
if ( '_manual_renewal' === trim( $_GET['_payment_method'] ) ) {
$meta_query = array(
array(
'key' => '_requires_manual_renewal',
'value' => 'true',
),
);
} else {
$payment_gateway_filter = ( 'none' == $_GET['_payment_method'] ) ? '' : $_GET['_payment_method']; $payment_gateway_filter = ( 'none' == $_GET['_payment_method'] ) ? '' : $_GET['_payment_method'];
$meta_query = array(
array(
'key' => '_payment_method',
'value' => $payment_gateway_filter,
),
);
}
$query_vars = array( $query_vars = array(
'post_type' => 'shop_subscription', 'post_type' => 'shop_subscription',
'posts_per_page' => -1, 'posts_per_page' => -1,
'post_status' => 'any', 'post_status' => 'any',
'fields' => 'ids', 'fields' => 'ids',
'meta_query' => array( 'meta_query' => $meta_query,
array(
'key' => '_payment_method',
'value' => $payment_gateway_filter,
),
),
); );
// If there are already set post restrictions (post__in) apply them to this query // If there are already set post restrictions (post__in) apply them to this query
@@ -934,7 +943,9 @@ class WCS_Admin_Post_Types {
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) { foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) {
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id == $gateway_id ? 'selected' : '' ) . '>' . esc_html( $gateway->title ) . '</option>'; echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id == $gateway_id ? 'selected' : '' ) . '>' . esc_html( $gateway->title ) . '</option>';
}?> }
echo '<option value="_manual_renewal">' . esc_html__( 'Manual Renewal', 'woocommerce-subscriptions' ) . '</option>';
?>
</select> <?php </select> <?php
} }
@@ -1034,7 +1045,7 @@ class WCS_Admin_Post_Types {
} }
$item_name .= apply_filters( 'woocommerce_order_item_name', $item['name'], $item, false ); $item_name .= apply_filters( 'woocommerce_order_item_name', $item['name'], $item, false );
$item_name = esc_html( $item_name ); $item_name = wp_kses_post( $item_name );
if ( 'include_quantity' === $include_quantity && $item_quantity > 1 ) { if ( 'include_quantity' === $include_quantity && $item_quantity > 1 ) {
$item_name = sprintf( '%s &times; %s', absint( $item_quantity ), $item_name ); $item_name = sprintf( '%s &times; %s', absint( $item_quantity ), $item_name );

View File

@@ -197,6 +197,22 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {
echo '</div>'; echo '</div>';
do_action( 'woocommerce_admin_order_data_after_billing_address', $subscription ); do_action( 'woocommerce_admin_order_data_after_billing_address', $subscription );
// Display a link to the customer's add/change payment method screen.
if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) {
if ( $subscription->has_payment_gateway() ) {
$link_text = __( 'Customer change payment method page &rarr;', 'woocommerce-subscriptions' );
} else {
$link_text = __( 'Customer add payment method page &rarr;', 'woocommerce-subscriptions' );
}
printf(
'<a href="%s">%s</a>',
esc_url( $subscription->get_change_payment_method_url() ),
esc_html( $link_text )
);
}
?> ?>
</div> </div>
<div class="order_data_column"> <div class="order_data_column">
@@ -302,8 +318,7 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {
// Ensure there is an order key. // Ensure there is an order key.
if ( ! $subscription->get_order_key() ) { if ( ! $subscription->get_order_key() ) {
$key = 'wc_' . apply_filters( 'woocommerce_generate_order_key', uniqid( 'order_' ) ); wcs_set_objects_property( $subscription, 'order_key', wcs_generate_order_key() );
wcs_set_objects_property( $subscription, 'order_key', $key );
} }
// Update meta // Update meta

View File

@@ -28,7 +28,7 @@ $order_post = wcs_get_objects_property( $order, 'post' );
if ( $timestamp_gmt > 0 ) { if ( $timestamp_gmt > 0 ) {
// translators: php date format // translators: php date format
$t_time = get_the_time( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $order_post ); $t_time = get_the_time( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $order_post );
$date_to_display = wcs_get_human_time_diff( $timestamp_gmt ); $date_to_display = ucfirst( wcs_get_human_time_diff( $timestamp_gmt ) );
} else { } else {
$t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' ); $t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' );
} ?> } ?>
@@ -37,7 +37,13 @@ $order_post = wcs_get_objects_property( $order, 'post' );
</abbr> </abbr>
</td> </td>
<td> <td>
<?php echo esc_html( ucwords( $order->get_status() ) ); ?> <?php
if ( wcs_is_subscription( $order ) ) {
echo esc_html( wcs_get_subscription_status_name( $order->get_status( 'view' ) ) );
} else {
echo esc_html( wc_get_order_status_name( $order->get_status( 'view' ) ) );
}
?>
</td> </td>
<td> <td>
<span class="amount"><?php echo wp_kses( $order->get_formatted_order_total(), array( 'small' => array(), 'span' => array( 'class' => array() ), 'del' => array(), 'ins' => array() ) ); ?></span> <span class="amount"><?php echo wp_kses( $order->get_formatted_order_total(), array( 'small' => array(), 'span' => array( 'class' => array() ), 'del' => array(), 'ins' => array() ) ); ?></span>

View File

@@ -42,7 +42,7 @@ if ( ! defined( 'ABSPATH' ) ) {
if ( $retry->get_time() > 0 ) { if ( $retry->get_time() > 0 ) {
// translators: php date format // translators: php date format
$t_time = date( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $retry->get_time() ); $t_time = date( _x( 'Y/m/d g:i:s A', 'post date', 'woocommerce-subscriptions' ), $retry->get_time() );
$date_to_display = wcs_get_human_time_diff( $retry->get_time() ); $date_to_display = ucfirst( wcs_get_human_time_diff( $retry->get_time() ) );
} else { } else {
$t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' ); $t_time = $date_to_display = __( 'Unpublished', 'woocommerce-subscriptions' );
} ?> } ?>

View File

@@ -27,35 +27,38 @@ class WCS_Report_Cache_Manager {
*/ */
private $update_events_and_classes = array( 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 '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 => 'WCS_Report_Subscription_Events_By_Date', 0 => 'WCS_Report_Dashboard',
1 => 'WCS_Report_Upcoming_Recurring_Revenue', 1 => 'WCS_Report_Subscription_Events_By_Date',
3 => 'WCS_Report_Subscription_By_Product', 2 => 'WCS_Report_Upcoming_Recurring_Revenue',
4 => 'WCS_Report_Subscription_By_Customer', 4 => 'WCS_Report_Subscription_By_Product',
5 => 'WCS_Report_Subscription_By_Customer',
), ),
'woocommerce_subscription_payment_complete' => array( // this hook takes care of renewal, switch and initial payments 'woocommerce_subscription_payment_complete' => array( // this hook takes care of renewal, switch and initial payments
0 => 'WCS_Report_Subscription_Events_By_Date', 0 => 'WCS_Report_Dashboard',
4 => 'WCS_Report_Subscription_By_Customer', 1 => 'WCS_Report_Subscription_Events_By_Date',
5 => 'WCS_Report_Subscription_By_Customer',
), ),
'woocommerce_subscriptions_switch_completed' => array( 'woocommerce_subscriptions_switch_completed' => array(
0 => 'WCS_Report_Subscription_Events_By_Date', 1 => 'WCS_Report_Subscription_Events_By_Date',
), ),
'woocommerce_subscription_status_changed' => array( 'woocommerce_subscription_status_changed' => array(
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 0 => 'WCS_Report_Dashboard',
4 => 'WCS_Report_Subscription_By_Customer', 1 => '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
5 => 'WCS_Report_Subscription_By_Customer',
), ),
'woocommerce_subscription_status_active' => array( 'woocommerce_subscription_status_active' => array(
1 => 'WCS_Report_Upcoming_Recurring_Revenue', 2 => 'WCS_Report_Upcoming_Recurring_Revenue',
), ),
'woocommerce_new_order_item' => array( 'woocommerce_new_order_item' => array(
3 => 'WCS_Report_Subscription_By_Product', 4 => 'WCS_Report_Subscription_By_Product',
), ),
'woocommerce_update_order_item' => array( 'woocommerce_update_order_item' => array(
3 => 'WCS_Report_Subscription_By_Product', 4 => 'WCS_Report_Subscription_By_Product',
), ),
); );
/** /**
* Record of all the report calsses to need to have the cache updated during this request. Prevents duplicate updates in the same request for different events. * Record of all the report classes to need to have the cache updated during this request. Prevents duplicate updates in the same request for different events.
*/ */
private $reports_to_update = array(); private $reports_to_update = array();
@@ -148,9 +151,9 @@ class WCS_Report_Cache_Manager {
$cron_args = array( 'report_class' => $report_class ); $cron_args = array( 'report_class' => $report_class );
if ( false === wp_next_scheduled( $this->cron_hook, $cron_args ) ) { if ( false === as_next_scheduled_action( $this->cron_hook, $cron_args ) ) {
// Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request // Use the index to space out caching of each report to make them 15 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
wp_schedule_single_event( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args ); as_schedule_single_action( $cache_update_timestamp + 15 * MINUTE_IN_SECONDS * ( $index + 1 ), $this->cron_hook, $cron_args );
} }
} }
} else { // Otherwise, run it 10 minutes after the last cache invalidating event } else { // Otherwise, run it 10 minutes after the last cache invalidating event
@@ -160,12 +163,12 @@ class WCS_Report_Cache_Manager {
$cron_args = array( 'report_class' => $report_class ); $cron_args = array( 'report_class' => $report_class );
if ( false !== ( $next_scheduled = wp_next_scheduled( $this->cron_hook, $cron_args ) ) ) { if ( false !== as_next_scheduled_action( $this->cron_hook, $cron_args ) ) {
wp_unschedule_event( $next_scheduled, $this->cron_hook, $cron_args ); as_unschedule_action( $this->cron_hook, $cron_args );
} }
// Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request // Use the index to space out caching of each report to make them 5 minutes apart so that on large sites, where we assume they'll get a request at least once every few minutes, we don't try to update the caches of all reports in the same request
wp_schedule_single_event( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args ); as_schedule_single_action( gmdate( 'U' ) + MINUTE_IN_SECONDS * ( $index + 1 ) * 5, $this->cron_hook, $cron_args );
} }
} }
} }

View File

@@ -30,13 +30,29 @@ class WCS_Report_Dashboard {
} }
/** /**
* Add the subscription specific details to the bottom of the dashboard widget * Get all data needed for this report and store in the class
*
* @since 2.1
*/ */
public static function add_stats_to_dashboard() { public static function get_data( $args = array() ) {
global $wpdb; global $wpdb;
$default_args = array(
'no_cache' => false,
);
$args = apply_filters( 'wcs_reports_subscription_dashboard_args', $args );
$args = wp_parse_args( $args, $default_args );
$offset = get_option( 'gmt_offset' );
// Use this once it is merged - wcs_get_gmt_offset_string();
// Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
$report_data = new stdClass;
$cached_results = get_transient( strtolower( self::class ) );
// Subscription signups this month
$query = $wpdb->prepare( $query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) AS count "SELECT COUNT(DISTINCT wcsubs.ID) AS count
FROM {$wpdb->posts} AS wcsubs FROM {$wpdb->posts} AS wcsubs
@@ -48,11 +64,51 @@ class WCS_Report_Dashboard {
AND wcorder.post_date >= '%s' AND wcorder.post_date >= '%s'
AND wcorder.post_date < '%s'", AND wcorder.post_date < '%s'",
date( 'Y-m-01', current_time( 'timestamp' ) ), date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
); );
$signup_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) ); $query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_query', $query ) );
set_transient( strtolower( __CLASS__ ), $cached_results, WEEK_IN_SECONDS );
}
$report_data->signup_count = $cached_results[ $query_hash ];
// Signup revenue this month
$query = $wpdb->prepare(
"SELECT SUM(order_total_meta.meta_value)
FROM {$wpdb->postmeta} AS order_total_meta
RIGHT JOIN
(
SELECT DISTINCT wcorder.ID
FROM {$wpdb->posts} AS wcsubs
INNER JOIN {$wpdb->posts} AS wcorder
ON wcsubs.post_parent = wcorder.ID
WHERE wcorder.post_type IN ( 'shop_order' )
AND wcsubs.post_type IN ( 'shop_subscription' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s'
AND wcorder.post_date < '%s'
) AS orders ON orders.ID = order_total_meta.post_id
WHERE order_total_meta.meta_key = '_order_total'",
date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
);
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_signup_revenue_query', $query ) );
set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS );
}
$report_data->signup_revenue = $cached_results[ $query_hash ];
// Subscription renewals this month
$query = $wpdb->prepare( $query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcorder.ID) AS count "SELECT COUNT(DISTINCT wcorder.ID) AS count
FROM {$wpdb->posts} AS wcorder FROM {$wpdb->posts} AS wcorder
@@ -67,20 +123,119 @@ class WCS_Report_Dashboard {
AND wcorder.post_date >= '%s' AND wcorder.post_date >= '%s'
AND wcorder.post_date < '%s'", AND wcorder.post_date < '%s'",
date( 'Y-m-01', current_time( 'timestamp' ) ), date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
); );
$renewal_count = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) ); $query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_query', $query ) );
set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS );
}
$report_data->renewal_count = $cached_results[ $query_hash ];
// Renewal revenue this month
$query = $wpdb->prepare(
"SELECT SUM(order_total_meta.meta_value)
FROM {$wpdb->postmeta} as order_total_meta
RIGHT JOIN
(
SELECT DISTINCT wcorder.ID
FROM {$wpdb->posts} AS wcorder
INNER JOIN {$wpdb->postmeta} AS meta__subscription_renewal
ON (
wcorder.id = meta__subscription_renewal.post_id
AND
meta__subscription_renewal.meta_key = '_subscription_renewal'
)
WHERE wcorder.post_type IN ( 'shop_order' )
AND wcorder.post_status IN ( 'wc-completed', 'wc-processing', 'wc-on-hold', 'wc-refunded' )
AND wcorder.post_date >= '%s'
AND wcorder.post_date < '%s'
) AS orders ON orders.ID = order_total_meta.post_id
WHERE order_total_meta.meta_key = '_order_total'",
date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
);
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_renewal_revenue_query', $query ) );
set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS );
}
$report_data->renewal_revenue = $cached_results[ $query_hash ];
// Cancellation count this month
$query = $wpdb->prepare(
"SELECT COUNT(DISTINCT wcsubs.ID) AS count
FROM {$wpdb->posts} AS wcsubs
JOIN {$wpdb->postmeta} AS wcsmeta_cancel
ON wcsubs.ID = wcsmeta_cancel.post_id
AND wcsmeta_cancel.meta_key = '_schedule_cancelled'
AND wcsubs.post_status NOT IN ( 'trash', 'auto-draft' )
AND CONVERT_TZ( wcsmeta_cancel.meta_value, '+00:00', '{$site_timezone}' ) BETWEEN '%s' AND '%s'",
date( 'Y-m-01', current_time( 'timestamp' ) ),
date( 'Y-m-d', strtotime( '+1 DAY', current_time( 'timestamp' ) ) )
);
$query_hash = md5( $query );
if ( $args['no_cache'] || false === $cached_results || ! isset( $cached_results[ $query_hash ] ) ) {
$wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' );
$cached_results[ $query_hash ] = $wpdb->get_var( apply_filters( 'woocommerce_subscription_dashboard_status_widget_cancellation_query', $query ) );
set_transient( strtolower( self::class ), $cached_results, HOUR_IN_SECONDS );
}
$report_data->cancel_count = $cached_results[ $query_hash ];
return $report_data;
}
/**
* Add the subscription specific details to the bottom of the dashboard widget
*
* @since 2.1
*/
public static function add_stats_to_dashboard() {
$report_data = self::get_data();
?> ?>
<li class="signup-count"> <li class="signup-count">
<a href="<?php echo esc_html( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date' ) ); ?>"> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date&range=month' ) ); ?>">
<?php printf( wp_kses_post( _n( '<strong>%s signup</strong> subscription signups this month', '<strong>%s signups</strong> subscription signups this month', $signup_count, 'woocommerce-subscriptions' ) ), esc_html( $signup_count ) ); ?> <?php
// translators: 1$: count, 2$ and 3$ are opening and closing strong tags, respectively.
echo wp_kses_post( sprintf( _n( '%2$s%$1s signup%3$s subscription signups this month', '%2$s%1$s signups%3$s subscription signups this month', $report_data->signup_count, 'woocommerce-subscriptions' ), $report_data->signup_count, '<strong>', '</strong>' ) );
?>
</a>
</li>
<li class="signup-revenue">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date&range=month' ) ); ?>">
<?php echo wp_kses_post( sprintf( __( '%s signup revenue this month', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $report_data->signup_revenue ) . '</strong>' ) ); ?>
</a> </a>
</li> </li>
<li class="renewal-count"> <li class="renewal-count">
<a href="<?php echo esc_html( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date' ) ); ?>"> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date&range=month' ) ); ?>">
<?php printf( wp_kses_post( _n( '<strong>%s renewal</strong> subscription renewals this month', '<strong>%s renewals</strong> subscription renewals this month', $renewal_count, 'woocommerce-subscriptions' ) ), esc_html( $renewal_count ) ); ?> <?php
// translators: 1$: count, 2$ and 3$ are opening and closing strong tags, respectively.
echo wp_kses_post( sprintf( _n( '%2$s%1$s renewal%3$s subscription renewals this month', '%2$s%1$s renewals%3$s subscription renewals this month', $report_data->renewal_count, 'woocommerce-subscriptions' ), $report_data->renewal_count, '<strong>', '</strong>' ) );
?>
</a>
</li>
<li class="renewal-revenue">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date&range=month' ) ); ?>">
<?php echo wp_kses_post( sprintf( __( '%s renewal revenue this month', 'woocommerce-subscriptions' ), '<strong>' . wc_price( $report_data->renewal_revenue ) . '</strong>' ) ); ?>
</a>
</li>
<li class="cancel-count">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-reports&tab=subscriptions&report=subscription_events_by_date&range=month' ) ); ?>">
<?php
// translators: 1$: count, 2$ and 3$ are opening and closing strong tags, respectively.
echo wp_kses_post( sprintf( _n( '%2$s%1$s cancellation%3$s subscription cancellations this month', '%2$s%1$s cancellations%3$s subscription cancellations this month', $report_data->cancel_count, 'woocommerce-subscriptions' ), $report_data->cancel_count, '<strong>', '</strong>' ) ); ?>
</a> </a>
</li> </li>
<?php <?php

View File

@@ -41,11 +41,11 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
echo '<div id="poststuff" class="woocommerce-reports-wide">'; echo '<div id="poststuff" class="woocommerce-reports-wide">';
echo ' <div id="postbox-container-1" class="postbox-container" style="width: 280px;"><div class="postbox" style="padding: 10px;">'; echo ' <div id="postbox-container-1" class="postbox-container" style="width: 280px;"><div class="postbox" style="padding: 10px;">';
echo ' <h3>' . esc_html__( 'Customer Totals', 'woocommerce-subscriptions' ) . '</h3>'; echo ' <h3>' . esc_html__( 'Customer Totals', 'woocommerce-subscriptions' ) . '</h3>';
echo ' <p><strong>' . esc_html__( 'Total Subscribers', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->total_customers ) . '<br />'; echo ' <p><strong>' . esc_html__( 'Total Subscribers', 'woocommerce-subscriptions' ) . '</strong>: ' . esc_html( $this->totals->total_customers ) . wcs_help_tip( __( 'The number of unique customers with a subscription of any status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '<br />';
echo ' <strong>' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->active_subscriptions ) . '<br />'; echo ' <strong>' . esc_html__( 'Active Subscriptions', 'woocommerce-subscriptions' ) . '</strong>: ' . esc_html( $this->totals->active_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status of active or pending cancellation.', 'woocommerce-subscriptions' ) ) . '<br />';
echo ' <strong>' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->total_subscriptions ) . '<br />'; echo ' <strong>' . esc_html__( 'Total Subscriptions', 'woocommerce-subscriptions' ) . '</strong>: ' . esc_html( $this->totals->total_subscriptions ) . wcs_help_tip( __( 'The total number of subscriptions with a status other than pending or trashed.', 'woocommerce-subscriptions' ) ) . '<br />';
echo ' <strong>' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . '</strong> : ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . '<br />'; echo ' <strong>' . esc_html__( 'Total Subscription Orders', 'woocommerce-subscriptions' ) . '</strong>: ' . esc_html( $this->totals->initial_order_count + $this->totals->renewal_switch_count ) . wcs_help_tip( __( 'The total number of sign-up, switch and renewal orders placed with your store with a paid status (i.e. processing or complete).', 'woocommerce-subscriptions' ) ) . '<br />';
echo ' <strong>' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . '</strong> : ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . '</p>'; echo ' <strong>' . esc_html__( 'Average Lifetime Value', 'woocommerce-subscriptions' ) . '</strong>: ' . wp_kses_post( wc_price( ( $this->totals->initial_order_total + $this->totals->renewal_switch_total ) / $this->totals->total_customers ) ) . wcs_help_tip( __( 'The average value of all customers\' sign-up, switch and renewal orders.', 'woocommerce-subscriptions' ) ) . '</p>';
echo '</div></div>'; echo '</div></div>';
$this->display(); $this->display();
echo '</div>'; echo '</div>';
@@ -75,7 +75,7 @@ class WCS_Report_Subscription_By_Customer extends WP_List_Table {
return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_subscription&_customer_user=' ), $user->customer_id, $user->total_subscriptions ); return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_subscription&_customer_user=' ), $user->customer_id, $user->total_subscriptions );
case 'total_subscription_order_count' : case 'total_subscription_order_count' :
return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_order&_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count ); return sprintf( '<a href="%s%d">%d</a>', admin_url( 'edit.php?post_type=shop_order&_paid_subscription_orders_for_customer_user=' ), $user->customer_id, $user->initial_order_count + $user->renewal_switch_count );
case 'customer_lifetime_value' : case 'customer_lifetime_value' :
return wc_price( $user->initial_order_total + $user->renewal_switch_total ); return wc_price( $user->initial_order_total + $user->renewal_switch_total );

View File

@@ -46,7 +46,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
$query_end_date = date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) ); $query_end_date = date( 'Y-m-d', strtotime( '+1 DAY', $this->end_date ) );
$offset = get_option( 'gmt_offset' ); $offset = get_option( 'gmt_offset' );
//Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query. // Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 ); $site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
$this->report_data = new stdClass; $this->report_data = new stdClass;
@@ -371,9 +371,9 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
$this->report_data->ended_counts = $cached_results[ $query_hash ]; $this->report_data->ended_counts = $cached_results[ $query_hash ];
// Total up the query data // Total up the query data
$this->report_data->signup_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) ) ); $this->report_data->signup_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->signup_data, 'signup_totals' ) );
$this->report_data->renewal_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) ); $this->report_data->renewal_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) );
$this->report_data->resubscribe_orders_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) ) ); $this->report_data->resubscribe_orders_total_amount = array_sum( wp_list_pluck( $this->report_data->resubscribe_data, 'resubscribe_totals' ) );
$this->report_data->new_subscription_total_count = absint( array_sum( wp_list_pluck( $this->report_data->new_subscriptions, 'count' ) ) ); $this->report_data->new_subscription_total_count = absint( array_sum( wp_list_pluck( $this->report_data->new_subscriptions, 'count' ) ) );
$this->report_data->signup_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'count' ) ) ); $this->report_data->signup_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->signup_data, 'count' ) ) );
$this->report_data->renewal_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) ); $this->report_data->renewal_orders_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) );
@@ -417,7 +417,7 @@ class WCS_Report_Subscription_Events_By_Date extends WC_Admin_Report {
$legend[] = array( $legend[] = array(
'title' => sprintf( __( '%s new subscriptions', 'woocommerce-subscriptions' ), '<strong>' . $this->report_data->new_subscription_total_count . '</strong>' ), 'title' => sprintf( __( '%s new subscriptions', 'woocommerce-subscriptions' ), '<strong>' . $this->report_data->new_subscription_total_count . '</strong>' ),
'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order.', 'woocommerce-subscriptions' ), 'placeholder' => __( 'The number of subscriptions created during this period, either by being manually created, imported or a customer placing an order. This includes orders pending payment.', 'woocommerce-subscriptions' ),
'color' => $this->chart_colours['new_count'], 'color' => $this->chart_colours['new_count'],
'highlight_series' => 1, 'highlight_series' => 1,
); );

View File

@@ -35,23 +35,39 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
private function query_report_data() { private function query_report_data() {
global $wpdb; global $wpdb;
// Convert from Decimal format(eg. 11.5) to a suitable format(eg. +11:30) for CONVERT_TZ() of SQL query.
$offset = get_option( 'gmt_offset' );
$site_timezone = sprintf( '%+02d:%02d', (int) $offset, ( $offset - floor( $offset ) ) * 60 );
$retry_date_in_local_time = $wpdb->prepare( "CONVERT_TZ(retries.date_gmt, '+00:00', %s)", $site_timezone );
// We need to compute this on our own since 'group_by_query' from the parent class uses posts table column names.
switch ( $this->chart_groupby ) {
case 'day':
$this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time}), DAY({$retry_date_in_local_time})";
break;
case 'month':
$this->group_by_query = "YEAR({$retry_date_in_local_time}), MONTH({$retry_date_in_local_time})";
break;
}
$this->report_data = new stdClass; $this->report_data = new stdClass;
$query_start_date = get_gmt_from_date( date( 'Y-m-d', $this->start_date ) ); $query_start_date = get_gmt_from_date( date( 'Y-m-d H:i:s', $this->start_date ) );
$query_end_date = get_gmt_from_date( date( 'Y-m-d', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) ); $query_end_date = get_gmt_from_date( date( 'Y-m-d H:i:s', wcs_strtotime_dark_knight( '+1 day', $this->end_date ) ) );
// Get the sum of order totals for completed retires (i.e. retries which eventually succeeded in processing the failed payment) // Get the sum of order totals for completed retries (i.e. retries which eventually succeeded in processing the failed payment)
$renewal_query = $wpdb->prepare( $renewal_query = $wpdb->prepare(
"SELECT COUNT(DISTINCT posts.ID) as count, posts.post_date as post_date, SUM(meta_order_total.meta_value) as renewal_totals "
FROM {$wpdb->prefix}posts AS orders SELECT COUNT(DISTINCT retries.retry_id) as count, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date, SUM(meta_order_total.meta_value) AS renewal_totals
INNER JOIN {$wpdb->prefix}posts AS posts ON ( orders.ID = posts.post_parent ) FROM {$wpdb->posts} AS orders
LEFT JOIN {$wpdb->prefix}postmeta AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' ) INNER JOIN {$wpdb->prefix}wcs_payment_retries AS retries ON ( orders.ID = retries.order_id )
WHERE posts.post_type = 'payment_retry' LEFT JOIN {$wpdb->postmeta} AS meta_order_total ON ( orders.ID = meta_order_total.post_id AND meta_order_total.meta_key = '_order_total' )
AND posts.post_status = 'complete' WHERE retries.status = 'complete'
AND posts.post_modified_gmt >= %s AND retries.date_gmt >= %s
AND posts.post_modified_gmt < %s AND retries.date_gmt < %s
GROUP BY {$this->group_by_query} GROUP BY {$this->group_by_query}
ORDER BY post_date ASC", ORDER BY retry_date_gmt ASC
",
$query_start_date, $query_start_date,
$query_end_date $query_end_date
); );
@@ -60,14 +76,15 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
// Get the counts for all retries, grouped by day or month and status // Get the counts for all retries, grouped by day or month and status
$retry_query = $wpdb->prepare( $retry_query = $wpdb->prepare(
"SELECT COUNT(DISTINCT posts.ID) as count, posts.post_status as status, posts.post_date as post_date "
FROM {$wpdb->prefix}posts AS posts SELECT COUNT(DISTINCT retries.retry_id) AS count, retries.status AS status, MIN(retries.date_gmt) AS retry_date_gmt, MIN({$retry_date_in_local_time}) AS retry_date
WHERE posts.post_type = 'payment_retry' FROM {$wpdb->prefix}wcs_payment_retries AS retries
AND posts.post_status IN ( 'complete','failed','pending' ) WHERE retries.status IN ( 'complete', 'failed', 'pending' )
AND posts.post_modified_gmt >= %s AND retries.date_gmt >= %s
AND posts.post_modified_gmt < %s AND retries.date_gmt < %s
GROUP BY {$this->group_by_query}, posts.post_status GROUP BY {$this->group_by_query}, status
ORDER BY posts.post_date_gmt ASC", ORDER BY retry_date_gmt ASC
",
$query_start_date, $query_start_date,
$query_end_date $query_end_date
); );
@@ -80,7 +97,7 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
$this->report_data->retry_pending_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'count' ) ) ); $this->report_data->retry_pending_count = absint( array_sum( wp_list_pluck( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'count' ) ) );
$this->report_data->renewal_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) ); $this->report_data->renewal_total_count = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'count' ) ) );
$this->report_data->renewal_total_amount = absint( array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) ) ); $this->report_data->renewal_total_amount = array_sum( wp_list_pluck( $this->report_data->renewal_data, 'renewal_totals' ) );
} }
/** /**
@@ -188,13 +205,13 @@ class WCS_Report_Subscription_Payment_Retry extends WC_Admin_Report {
global $wp_locale; global $wp_locale;
// Prepare data for report // Prepare data for report
$retry_count = $this->prepare_chart_data( $this->report_data->retry_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $retry_count = $this->prepare_chart_data( $this->report_data->retry_data, 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$retry_success_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $retry_success_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'complete' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$retry_failure_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $retry_failure_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'failed' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$retry_pending_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $retry_pending_count = $this->prepare_chart_data( wp_list_filter( $this->report_data->retry_data, array( 'status' => 'pending' ) ), 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$renewal_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $renewal_count = $this->prepare_chart_data( $this->report_data->renewal_data, 'retry_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby );
$renewal_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'post_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby ); $renewal_amount = $this->prepare_chart_data( $this->report_data->renewal_data, 'retry_date', 'renewal_totals', $this->chart_interval, $this->start_date, $this->chart_groupby );
// Encode in json format // Encode in json format
$chart_data = array( $chart_data = array(

View File

@@ -121,7 +121,7 @@ class WC_API_Subscriptions extends WC_API_Orders {
public function get_subscriptions( $fields = null, $filter = array(), $status = null, $page = 1 ) { public function get_subscriptions( $fields = null, $filter = array(), $status = null, $page = 1 ) {
// check user permissions // check user permissions
if ( ! current_user_can( 'read_private_shop_orders' ) ) { if ( ! current_user_can( 'read_private_shop_orders' ) ) {
return new WP_Error( 'wcs_api_user_cannot_read_susbcription_count', __( 'You do not have permission to read the subscriptions count', 'woocommerce-subscriptions' ), array( 'status' => 401 ) ); return new WP_Error( 'wcs_api_user_cannot_read_subscription_count', __( 'You do not have permission to read the subscriptions count', 'woocommerce-subscriptions' ), array( 'status' => 401 ) );
} }
$status = $this->format_statuses( $status ); $status = $this->format_statuses( $status );

View File

@@ -329,6 +329,8 @@ class WC_Subscription extends WC_Order {
$can_be_updated = true; $can_be_updated = true;
} elseif ( $this->has_status( 'pending' ) ) { } elseif ( $this->has_status( 'pending' ) ) {
$can_be_updated = true; $can_be_updated = true;
} elseif ( $this->has_status( 'pending-cancel' ) && ( $this->is_manual() || ( false === $this->payment_method_supports( 'gateway_scheduled_payments' ) && $this->payment_method_supports( 'subscription_date_changes' ) && $this->payment_method_supports( 'subscription_reactivation' ) ) ) ) {
$can_be_updated = true;
} else { } else {
$can_be_updated = false; $can_be_updated = false;
} }
@@ -450,6 +452,14 @@ class WC_Subscription extends WC_Order {
case 'completed' : // core WC order status mapped internally to avoid exceptions case 'completed' : // core WC order status mapped internally to avoid exceptions
case 'active' : case 'active' :
if ( 'pending-cancel' === $old_status ) {
$this->update_dates( array(
'cancelled' => 0,
'end' => 0,
'next_payment' => $this->get_date( 'end' ),
) );
} else {
// Recalculate and set next payment date // Recalculate and set next payment date
$stored_next_payment = $this->get_time( 'next_payment' ); $stored_next_payment = $this->get_time( 'next_payment' );
@@ -467,6 +477,8 @@ class WC_Subscription extends WC_Order {
// In case plugins want to run some code when the subscription was reactivated, but the next payment date was not recalculated. // In case plugins want to run some code when the subscription was reactivated, but the next payment date was not recalculated.
do_action( 'woocommerce_subscription_activation_next_payment_not_recalculated', $stored_next_payment, $old_status, $this ); do_action( 'woocommerce_subscription_activation_next_payment_not_recalculated', $stored_next_payment, $old_status, $this );
} }
}
// Trial end date and end/expiration date don't change at all - they should be set when the subscription is first created // Trial end date and end/expiration date don't change at all - they should be set when the subscription is first created
wcs_make_user_active( $this->get_user_id() ); wcs_make_user_active( $this->get_user_id() );
break; break;
@@ -528,34 +540,54 @@ class WC_Subscription extends WC_Order {
* Handle the status transition. * Handle the status transition.
*/ */
protected function status_transition() { protected function status_transition() {
// Use local copy of status transition value.
$status_transition = $this->status_transition;
if ( $this->status_transition ) { // If we're not currently in the midst of a status transition, bail early.
do_action( 'woocommerce_subscription_status_' . $this->status_transition['to'], $this ); if ( ! $status_transition ) {
return;
}
if ( ! empty( $this->status_transition['from'] ) ) { try {
do_action( "woocommerce_subscription_status_{$status_transition['to']}", $this );
if ( ! empty( $status_transition['from'] ) ) {
$transition_note = sprintf(
/* translators: 1: old subscription status 2: new subscription status */ /* translators: 1: old subscription status 2: new subscription status */
$transition_note = sprintf( __( 'Status changed from %1$s to %2$s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $this->status_transition['from'] ), wcs_get_subscription_status_name( $this->status_transition['to'] ) ); __( 'Status changed from %1$s to %2$s.', 'woocommerce-subscriptions' ),
wcs_get_subscription_status_name( $status_transition['from'] ),
wcs_get_subscription_status_name( $status_transition['to'] )
);
do_action( 'woocommerce_subscription_status_' . $this->status_transition['from'] . '_to_' . $this->status_transition['to'], $this ); do_action( "woocommerce_subscription_status_{$status_transition['from']}_to_{$status_transition['to']}", $this );
// Trigger a hook with params we want // Trigger a hook with params we want.
do_action( 'woocommerce_subscription_status_updated', $this, $this->status_transition['to'], $this->status_transition['from'] ); do_action( 'woocommerce_subscription_status_updated', $this, $status_transition['to'], $status_transition['from'] );
// Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes
do_action( 'woocommerce_subscription_status_changed', $this->get_id(), $this->status_transition['from'], $this->status_transition['to'], $this );
// Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes.
do_action( 'woocommerce_subscription_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this );
} else { } else {
/* translators: %s: new order status */ /* translators: %s: new order status */
$transition_note = sprintf( __( 'Status set to %s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $this->status_transition['to'] ) ); $transition_note = sprintf( __( 'Status set to %s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $status_transition['to'] ) );
} }
// Note the transition occured // Note the transition occurred.
$this->add_order_note( trim( $this->status_transition['note'] . ' ' . $transition_note ), 0, $this->status_transition['manual'] ); $this->add_order_note( trim( "{$status_transition['note']} {$transition_note}" ), 0, $status_transition['manual'] );
} catch ( Exception $e ) {
$logger = wc_get_logger();
$logger->error(
sprintf( 'Status transition of subscription #%d errored!', $this->get_id() ),
array(
'order' => $this,
'error' => $e,
)
);
$this->add_order_note( __( 'Error during subscription status transition.', 'woocommerce-subscriptions' ) . ' ' . $e->getMessage() );
}
// This has ran, so reset status transition variable // This has run, so reset status transition variable
$this->status_transition = false; $this->status_transition = false;
} }
}
/** /**
* Checks if the subscription requires manual renewal payments. * Checks if the subscription requires manual renewal payments.
@@ -1799,10 +1831,13 @@ class WC_Subscription extends WC_Order {
$related_orders = array(); $related_orders = array();
foreach ( $order_types as $order_type ) { foreach ( $order_types as $order_type ) {
$related_orders_for_order_type = array(); $related_orders_for_order_type = array();
foreach ( $this->get_related_order_ids( $order_type ) as $order_id ) { foreach ( $this->get_related_order_ids( $order_type ) as $order_id ) {
$related_orders_for_order_type[ $order_id ] = ( 'all' == $return_fields ) ? wc_get_order( $order_id ) : $order_id; if ( 'all' === $return_fields && $order = wc_get_order( $order_id ) ) {
$related_orders_for_order_type[ $order_id ] = $order;
} elseif ( 'ids' === $return_fields ) {
$related_orders_for_order_type[ $order_id ] = $order_id;
}
} }
$related_orders += apply_filters( 'woocommerce_subscription_related_orders', $related_orders_for_order_type, $this, $return_fields, $order_type ); $related_orders += apply_filters( 'woocommerce_subscription_related_orders', $related_orders_for_order_type, $this, $return_fields, $order_type );
@@ -1886,9 +1921,11 @@ class WC_Subscription extends WC_Order {
/** /**
* Determine how the payment method should be displayed for a subscription. * Determine how the payment method should be displayed for a subscription.
* *
* @param string $context The context the payment method is being displayed in. Can be 'admin' or 'customer'. Default 'admin'.
*
* @since 2.0 * @since 2.0
*/ */
public function get_payment_method_to_display() { public function get_payment_method_to_display( $context = 'admin' ) {
if ( $this->is_manual() ) { if ( $this->is_manual() ) {
@@ -1899,14 +1936,25 @@ class WC_Subscription extends WC_Order {
$payment_method_to_display = $payment_gateway->get_title(); $payment_method_to_display = $payment_gateway->get_title();
// Fallback to the title of the payment method when the subscripion was created // Fallback to the title of the payment method when the subscription was created
} else { } else {
$payment_method_to_display = $this->get_payment_method_title(); $payment_method_to_display = $this->get_payment_method_title();
} }
return apply_filters( 'woocommerce_subscription_payment_method_to_display', $payment_method_to_display, $this ); $payment_method_to_display = apply_filters( 'woocommerce_subscription_payment_method_to_display', $payment_method_to_display, $this, $context );
if ( 'customer' === $context ) {
$payment_method_to_display = sprintf( __( 'Via %s', 'woocommerce-subscriptions' ), $payment_method_to_display );
// Only filter the result for non-manual subscriptions.
if ( ! $this->is_manual() ) {
$payment_method_to_display = apply_filters( 'woocommerce_my_subscriptions_payment_method', $payment_method_to_display, $this );
}
}
return $payment_method_to_display;
} }
/** /**
@@ -2029,6 +2077,16 @@ class WC_Subscription extends WC_Order {
return $has_product; return $has_product;
} }
/**
* Check if the subscription has a payment gateway.
*
* @since 2.5.0
* @return bool
*/
public function has_payment_gateway() {
return (bool) wc_get_payment_gateway_by_order( $this );
}
/** /**
* The total sign-up fee for the subscription if any. * The total sign-up fee for the subscription if any.
* *
@@ -2355,7 +2413,17 @@ class WC_Subscription extends WC_Order {
} }
/** /**
* Get the subscription's payment method meta. * Generates a URL to add or change the subscription's payment method from the my account page.
*
* @return string
* @since 2.5.0
*/
public function get_change_payment_method_url() {
$change_payment_method_url = wc_get_endpoint_url( 'subscription-payment-method', $this->get_id(), wc_get_page_permalink( 'myaccount' ) );
return apply_filters( 'wcs_get_change_payment_method_url', $change_payment_method_url, $this->get_id() );
}
/* Get the subscription's payment method meta.
* *
* @since 2.4.3 * @since 2.4.3
* @return array The subscription's payment meta in the format returned by the woocommerce_subscription_payment_meta filter. * @return array The subscription's payment meta in the format returned by the woocommerce_subscription_payment_meta filter.
@@ -2396,6 +2464,23 @@ class WC_Subscription extends WC_Order {
return null; return null;
} }
/**
* Get totals for display on pages and in emails.
*
* @param mixed $tax_display Excl or incl tax display mode.
* @return array
*/
public function get_order_item_totals( $tax_display = '' ) {
$total_rows = parent::get_order_item_totals( $tax_display );
// Use get_payment_method_to_display() as it displays "Manual Renewal" for manual subscriptions.
if ( isset( $total_rows['payment_method'] ) ) {
$total_rows['payment_method']['value'] = $this->get_payment_method_to_display( 'customer' );
}
return apply_filters( 'woocommerce_get_subscription_item_totals', $total_rows, $this, $tax_display );
}
/************************ /************************
* Deprecated Functions * * Deprecated Functions *
************************/ ************************/

View File

@@ -80,13 +80,6 @@ class WC_Subscriptions_Cart {
// Make sure cart product prices correctly include/exclude taxes // Make sure cart product prices correctly include/exclude taxes
add_filter( 'woocommerce_cart_product_price', __CLASS__ . '::cart_product_price' , 10, 2 ); add_filter( 'woocommerce_cart_product_price', __CLASS__ . '::cart_product_price' , 10, 2 );
// Make sure cart totals are calculated when setting up the cart widget
add_action( 'wc_ajax_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments' , 1 );
add_action( 'wp_ajax_woocommerce_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments', 1 );
add_action( 'wp_ajax_nopriv_woocommerce_get_refreshed_fragments', __CLASS__ . '::pre_get_refreshed_fragments', 1, 1 );
add_action( 'woocommerce_ajax_added_to_cart', __CLASS__ . '::pre_get_refreshed_fragments', 1, 1 );
// Display grouped recurring amounts after order totals on the cart/checkout pages // Display grouped recurring amounts after order totals on the cart/checkout pages
add_action( 'woocommerce_cart_totals_after_order_total', __CLASS__ . '::display_recurring_totals' ); 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_review_order_after_order_total', __CLASS__ . '::display_recurring_totals' );
@@ -797,6 +790,17 @@ class WC_Subscriptions_Cart {
return $cart_contains_free_trial; return $cart_contains_free_trial;
} }
/**
* Checks to see if payment method is required on a subscription product with a $0 initial payment.
*
* @since 2.5.0
*/
public static function zero_initial_payment_requires_payment() {
return 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_zero_initial_payment_requires_payment', 'no' );
}
/** /**
* Gets the cart calculation type flag * Gets the cart calculation type flag
* *
@@ -860,12 +864,28 @@ class WC_Subscriptions_Cart {
* @return bool * @return bool
*/ */
public static function cart_needs_payment( $needs_payment, $cart ) { public static function cart_needs_payment( $needs_payment, $cart ) {
if ( false === $needs_payment && self::cart_contains_subscription() && $cart->total == 0 && false === WC_Subscriptions_Switcher::cart_contains_switches() && 'yes' !== get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) {
// Skip checks if needs payment is already set or cart total not 0.
if ( false !== $needs_payment || 0 != $cart->total ) {
return $needs_payment;
}
// Skip checks if new $0 initial payments don't require a payment method or cart has no subscriptions.
if ( ! self::zero_initial_payment_requires_payment() || ! self::cart_contains_subscription() ) {
return $needs_payment;
}
// Skip checks if cart contains subscription switches or automatic payments are disabled.
if ( false !== WC_Subscriptions_Switcher::cart_contains_switches() || 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) {
return $needs_payment;
}
$recurring_total = 0; $recurring_total = 0;
$is_one_period = true; $is_one_period = true;
$contains_synced = false; $contains_synced = false;
$contains_expiring_limited_coupon = false; $contains_expiring_limited_coupon = false;
if ( ! empty( WC()->cart->recurring_carts ) ) {
foreach ( WC()->cart->recurring_carts as $recurring_cart ) { foreach ( WC()->cart->recurring_carts as $recurring_cart ) {
$recurring_total += $recurring_cart->total; $recurring_total += $recurring_cart->total;
$subscription_length = wcs_cart_pluck( $recurring_cart, 'subscription_length' ); $subscription_length = wcs_cart_pluck( $recurring_cart, 'subscription_length' );
@@ -876,12 +896,12 @@ class WC_Subscriptions_Cart {
$is_one_period = false; $is_one_period = false;
} }
} }
$has_trial = self::cart_contains_free_trial();
if ( $contains_expiring_limited_coupon || $recurring_total > 0 && ( ! $is_one_period || $has_trial || $contains_synced ) ) {
$needs_payment = true;
} }
$needs_trial_payment = self::cart_contains_free_trial();
if ( $contains_expiring_limited_coupon || $recurring_total > 0 && ( ! $is_one_period || $needs_trial_payment || $contains_synced ) ) {
$needs_payment = true;
} }
return $needs_payment; return $needs_payment;
@@ -963,19 +983,6 @@ class WC_Subscriptions_Cart {
return $price; return $price;
} }
/**
* Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method
* so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string.
*
* @since 1.5.11
*/
public static function pre_get_refreshed_fragments() {
if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) {
define( 'WOOCOMMERCE_CART', true );
WC()->cart->calculate_totals();
}
}
/** /**
* Display the recurring totals for items in the cart * Display the recurring totals for items in the cart
* *
@@ -1343,6 +1350,21 @@ class WC_Subscriptions_Cart {
/* Deprecated */ /* Deprecated */
/**
* Make sure cart totals are calculated when the cart widget is populated via the get_refreshed_fragments() method
* so that @see self::get_formatted_cart_subtotal() returns the correct subtotal price string.
*
* @since 1.5.11
* @deprecated 2.5.0
*/
public static function pre_get_refreshed_fragments() {
wcs_deprecated_function( __METHOD__, '2.5.0' );
if ( defined( 'DOING_AJAX' ) && true === DOING_AJAX && ! defined( 'WOOCOMMERCE_CART' ) ) {
define( 'WOOCOMMERCE_CART', true );
WC()->cart->calculate_totals();
}
}
/** /**
* Checks the cart to see if it contains a subscription product renewal. * Checks the cart to see if it contains a subscription product renewal.
* *

View File

@@ -67,6 +67,10 @@ class WC_Subscriptions_Change_Payment_Gateway {
// Maybe filter subscriptions_needs_payment to return false when processing change-payment-gateway requests // 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 ); add_filter( 'woocommerce_subscription_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 1 );
// Display a login form if the customer is requesting to change their payment method but aren't logged in.
add_filter( 'the_content', array( __CLASS__, 'maybe_request_log_in' ) );
} }
/** /**
@@ -310,11 +314,16 @@ class WC_Subscriptions_Change_Payment_Gateway {
if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) { if ( $subscription->can_be_updated_to( 'new-payment-method' ) ) {
if ( $subscription->has_payment_gateway() && wc_get_payment_gateway_by_order( $subscription )->supports( 'subscriptions' ) ) {
$action_name = _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' );
} else {
$action_name = _x( 'Add Payment', 'label on button, imperative', 'woocommerce-subscriptions' );
}
$actions['change_payment_method'] = array( $actions['change_payment_method'] = array(
'url' => wp_nonce_url( add_query_arg( array( 'change_payment_method' => $subscription->get_id() ), $subscription->get_checkout_payment_url() ) ), 'url' => wp_nonce_url( add_query_arg( array( 'change_payment_method' => $subscription->get_id() ), $subscription->get_checkout_payment_url() ) ),
'name' => _x( 'Change Payment', 'label on button, imperative', 'woocommerce-subscriptions' ), 'name' => $action_name,
); );
} }
return $actions; return $actions;
@@ -331,9 +340,12 @@ class WC_Subscriptions_Change_Payment_Gateway {
*/ */
public static function change_payment_method_via_pay_shortcode() { public static function change_payment_method_via_pay_shortcode() {
if ( isset( $_POST['_wcsnonce'] ) && wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) ) { if ( ! isset( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_change_payment_method' ) ) {
return;
}
$subscription = wcs_get_subscription( absint( $_POST['woocommerce_change_payment'] ) ); $subscription_id = absint( $_POST['woocommerce_change_payment'] );
$subscription = wcs_get_subscription( $subscription_id );
do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription ); do_action( 'woocommerce_subscription_change_payment_method_via_pay_shortcode', $subscription );
@@ -366,6 +378,7 @@ class WC_Subscriptions_Change_Payment_Gateway {
// Update payment method // Update payment method
$new_payment_method = wc_clean( $_POST['payment_method'] ); $new_payment_method = wc_clean( $_POST['payment_method'] );
$notice = $subscription->has_payment_gateway() ? __( 'Payment method updated.', 'woocommerce-subscriptions' ) : __( 'Payment method added.', 'woocommerce-subscriptions' );
// Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed // Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed
if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) { if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) {
@@ -388,24 +401,141 @@ class WC_Subscriptions_Change_Payment_Gateway {
$result = apply_filters( 'woocommerce_subscriptions_process_payment_for_change_method_via_pay_shortcode', $result, $subscription ); $result = apply_filters( 'woocommerce_subscriptions_process_payment_for_change_method_via_pay_shortcode', $result, $subscription );
if ( 'success' != $result['result'] ) {
return;
}
$subscription->set_requires_manual_renewal( false );
$subscription->save();
// Does the customer want all current subscriptions to be updated to this payment method?
if (
isset( $_POST['update_all_subscriptions_payment_method'] )
&& $_POST['update_all_subscriptions_payment_method']
&& WC_Subscriptions_Change_Payment_Gateway::can_update_all_subscription_payment_methods( $available_gateways[ $new_payment_method ], $subscription )
) {
// Allow some payment gateways which can't process the payment immediately, like PayPal, to do it later after the payment/sign-up is confirmed
if ( ! apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $new_payment_method, $subscription ) ) {
$subscription->update_meta_data( '_delayed_update_payment_method_all', $new_payment_method );
$subscription->save();
$notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-subscriptions' );
} elseif ( self::update_all_payment_methods_from_subscription( $subscription, $new_payment_method ) ) {
$notice = __( 'Payment method updated for all your current subscriptions.', 'woocommerce-subscriptions' );
}
}
// Redirect to success/confirmation/payment page // Redirect to success/confirmation/payment page
if ( 'success' == $result['result'] ) { wc_add_notice( $notice );
wc_add_notice( __( 'Payment method updated.', 'woocommerce-subscriptions' ), 'success' );
wp_redirect( $result['redirect'] ); wp_redirect( $result['redirect'] );
exit; exit;
} }
} }
ob_get_clean();
} }
/**
* Update the recurring payment method on all current subscriptions to the payment method on this subscription.
*
* @param WC_Subscription $subscription An instance of a WC_Subscription object.
* @param string $new_payment_method The ID of the new payment method.
* @return bool Were other subscriptions updated.
* @since 2.5.0
*/
public static function update_all_payment_methods_from_subscription( $subscription, $new_payment_method ) {
// Require the delayed payment update method to match the current gateway if it is set
if ( self::will_subscription_update_all_payment_methods( $subscription ) ) {
if ( $subscription->get_meta( '_delayed_update_payment_method_all' ) != $new_payment_method ) {
return false;
} }
$subscription->delete_meta_data( '_delayed_update_payment_method_all' );
$subscription->save_meta_data();
}
$payment_meta_table = WCS_Payment_tokens::get_subscription_payment_meta( $subscription, $new_payment_method );
if ( ! is_array( $payment_meta_table ) ) {
return false;
}
$subscription_ids = WCS_Customer_Store::instance()->get_users_subscription_ids( $subscription->get_customer_id() );
foreach ( $subscription_ids as $subscription_id ) {
// Skip the subscription providing the new payment meta.
if ( $subscription->get_id() == $subscription_id ) {
continue;
}
$user_subscription = wcs_get_subscription( $subscription_id );
// Skip if subscription's current payment method is not supported
if ( ! $user_subscription->payment_method_supports( 'subscription_cancellation' ) ) {
continue;
}
// Skip if there are no remaining payments or the subscription is not current.
if ( $user_subscription->get_time( 'next_payment' ) <= 0 || ! $user_subscription->has_status( array( 'active', 'on-hold' ) ) ) {
continue;
}
self::update_payment_method( $user_subscription, $new_payment_method, $payment_meta_table );
$user_subscription->set_requires_manual_renewal( false );
$user_subscription->save();
}
return true;
}
/**
* Check whether a payment method supports updating all current subscriptions' payment method.
*
* @param WC_Payment_Gateway $gateway The payment gateway to check.
* @param WC_Subscription $subscription An instance of a WC_Subscription object.
* @return bool Gateway supports updating all current subscriptions.
* @since 2.5.0
*/
public static function can_update_all_subscription_payment_methods( $gateway, $subscription ) {
if ( $gateway->supports( 'subscription_payment_method_delayed_change' ) ) {
return true;
}
if ( ! $gateway->supports( 'subscription_payment_method_change_admin' ) ) {
return false;
}
if ( apply_filters( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', true, $gateway->id, $subscription ) ) {
return true;
}
return false;
}
/**
* Check whether a subscription will update all current subscriptions' payment method.
*
* @param WC_Subscription $subscription An instance of a WC_Subscription object.
* @return bool Subscription will update all current subscriptions.
* @since 2.5.0
*/
public static function will_subscription_update_all_payment_methods( $subscription ) {
if ( ! wcs_is_subscription( $subscription ) ) {
return false;
}
return (bool) $subscription->get_meta( '_delayed_update_payment_method_all' );
} }
/** /**
* Update the recurring payment method on a subscription order. * Update the recurring payment method on a subscription order.
* *
* @param array $available_gateways The payment gateways which are currently being allowed. * @param WC_Subscription $subscription An instance of a WC_Subscription object.
* @param string $new_payment_method The ID of the new payment method.
* @param array $new_payment_method_meta The meta for the new payment method. Optional. Default false.
* @since 1.4 * @since 1.4
*/ */
public static function update_payment_method( $subscription, $new_payment_method ) { public static function update_payment_method( $subscription, $new_payment_method, $new_payment_method_meta = false ) {
$old_payment_method = $subscription->get_payment_method(); $old_payment_method = $subscription->get_payment_method();
$old_payment_method_title = $subscription->get_payment_method_title(); $old_payment_method_title = $subscription->get_payment_method_title();
@@ -417,18 +547,12 @@ class WC_Subscriptions_Change_Payment_Gateway {
WC_Subscriptions_Payment_Gateways::trigger_gateway_status_updated_hook( $subscription, 'cancelled' ); WC_Subscriptions_Payment_Gateways::trigger_gateway_status_updated_hook( $subscription, 'cancelled' );
// Update meta // Update meta
update_post_meta( $subscription->get_id(), '_old_payment_method', $old_payment_method );
update_post_meta( $subscription->get_id(), '_payment_method', $new_payment_method );
if ( isset( $available_gateways[ $new_payment_method ] ) ) { if ( isset( $available_gateways[ $new_payment_method ] ) ) {
$new_payment_method_title = $available_gateways[ $new_payment_method ]->get_title(); $new_payment_method_title = $available_gateways[ $new_payment_method ]->get_title();
} else { } else {
$new_payment_method_title = ''; $new_payment_method_title = '';
} }
update_post_meta( $subscription->get_id(), '_old_payment_method_title', $old_payment_method_title );
update_post_meta( $subscription->get_id(), '_payment_method_title', $new_payment_method_title );
if ( empty( $old_payment_method_title ) ) { if ( empty( $old_payment_method_title ) ) {
$old_payment_method_title = $old_payment_method; $old_payment_method_title = $old_payment_method;
} }
@@ -437,13 +561,22 @@ class WC_Subscriptions_Change_Payment_Gateway {
$new_payment_method_title = $new_payment_method; $new_payment_method_title = $new_payment_method;
} }
$subscription->update_meta_data( '_old_payment_method', $old_payment_method );
$subscription->update_meta_data( '_old_payment_method_title', $old_payment_method_title );
$subscription->set_payment_method( $new_payment_method, $new_payment_method_meta );
$subscription->set_payment_method_title( $new_payment_method_title );
// Log change on order // Log change on order
$subscription->add_order_note( sprintf( _x( 'Payment method changed from "%1$s" to "%2$s" by the subscriber from their account page.', '%1$s: old payment title, %2$s: new payment title', 'woocommerce-subscriptions' ), $old_payment_method_title, $new_payment_method_title ) ); $subscription->add_order_note( sprintf( _x( 'Payment method changed from "%1$s" to "%2$s" by the subscriber from their account page.', '%1$s: old payment title, %2$s: new payment title', 'woocommerce-subscriptions' ), $old_payment_method_title, $new_payment_method_title ) );
$subscription->save();
do_action( 'woocommerce_subscription_payment_method_updated', $subscription, $new_payment_method, $old_payment_method ); do_action( 'woocommerce_subscription_payment_method_updated', $subscription, $new_payment_method, $old_payment_method );
do_action( 'woocommerce_subscription_payment_method_updated_to_' . $new_payment_method, $subscription, $old_payment_method ); do_action( 'woocommerce_subscription_payment_method_updated_to_' . $new_payment_method, $subscription, $old_payment_method );
if ( $old_payment_method ) {
do_action( 'woocommerce_subscription_payment_method_updated_from_' . $old_payment_method, $subscription, $new_payment_method ); do_action( 'woocommerce_subscription_payment_method_updated_from_' . $old_payment_method, $subscription, $new_payment_method );
} }
}
/** /**
* Only display gateways which support changing payment method when paying for a failed renewal order or * Only display gateways which support changing payment method when paying for a failed renewal order or
@@ -552,26 +685,39 @@ class WC_Subscriptions_Change_Payment_Gateway {
* For the recurring payment method to be changeable, the subscription must be active, have future (automatic) payments * For the recurring payment method to be changeable, the subscription must be active, have future (automatic) payments
* and use a payment gateway which allows the subscription to be cancelled. * and use a payment gateway which allows the subscription to be cancelled.
* *
* @param bool $subscription_can_be_changed Flag of whether the subscription can be changed to * @param bool $subscription_can_be_changed Flag of whether the subscription can be changed.
* @param string $new_status_or_meta The status or meta data you want to change th subscription to. Can be 'active', 'on-hold', 'cancelled', 'expired', 'trash', 'deleted', 'failed', 'new-payment-date' or some other value attached to the 'woocommerce_can_subscription_be_changed_to' filter. * @param WC_Subscription $subscription The subscription to check.
* @param object $args Set of values used in @see WC_Subscriptions_Manager::can_subscription_be_changed_to() for determining if a subscription can be changes, include: * @return bool Flag indicating whether the subscription payment method can be updated.
* 'subscription_key' string A subscription key of the form created by @see WC_Subscriptions_Manager::get_subscription_key()
* 'subscription' array Subscription of the form returned by @see WC_Subscriptions_Manager::get_subscription()
* 'user_id' int The ID of the subscriber.
* 'order' WC_Order The order which recorded the successful payment (to make up for the failed automatic payment).
* 'payment_gateway' WC_Payment_Gateway The subscription's recurring payment gateway
* 'order_uses_manual_payments' bool A boolean flag indicating whether the subscription requires manual renewal payment.
* @since 1.4 * @since 1.4
*/ */
public static function can_subscription_be_updated_to_new_payment_method( $subscription_can_be_changed, $subscription ) { public static function can_subscription_be_updated_to_new_payment_method( $subscription_can_be_changed, $subscription ) {
if ( WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_customer' ) && $subscription->get_time( 'next_payment' ) > 0 && ! $subscription->is_manual() && $subscription->payment_method_supports( 'subscription_cancellation' ) && $subscription->has_status( 'active' ) ) { // Don't allow if automatic payments are disabled and the toggle is also disabled.
$subscription_can_be_changed = true; if ( 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) && ! WCS_My_Account_Auto_Renew_Toggle::is_enabled() ) {
} else { return false;
$subscription_can_be_changed = false;
} }
return $subscription_can_be_changed; // If there's no recurring payment, there's no need to add or update the payment method.
if ( $subscription->get_total() == 0 ) {
return false;
}
// Don't allow if no gateways support changing methods.
if ( ! WC_Subscriptions_Payment_Gateways::one_gateway_supports( 'subscription_payment_method_change_customer' ) ) {
return false;
}
// Don't allow if there are no remaining payments or the subscription is not active.
if ( $subscription->get_time( 'next_payment' ) <= 0 || ! $subscription->has_status( 'active' ) ) {
return false;
}
// Don't allow on subscription that doesn't support changing methods.
if ( ! $subscription->payment_method_supports( 'subscription_cancellation' ) ) {
return false;
}
return true;
} }
/** /**
@@ -583,8 +729,22 @@ class WC_Subscriptions_Change_Payment_Gateway {
*/ */
public static function change_payment_method_page_title( $title ) { public static function change_payment_method_page_title( $title ) {
if ( is_main_query() && in_the_loop() && is_page() && is_checkout_pay_page() && self::$is_request_to_change_payment ) { global $wp;
// Skip if not on checkout pay page or not a payment change request.
if ( ! self::$is_request_to_change_payment || ! is_main_query() || ! in_the_loop() || ! is_page() || ! is_checkout_pay_page() ) {
return $title;
}
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
if ( ! $subscription ) {
return $title;
}
if ( $subscription->has_payment_gateway() ) {
$title = _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ); $title = _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' );
} else {
$title = _x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' );
} }
return $title; return $title;
@@ -617,10 +777,17 @@ class WC_Subscriptions_Change_Payment_Gateway {
esc_url( $subscription->get_view_order_url() ), esc_url( $subscription->get_view_order_url() ),
); );
if ( $subscription->has_payment_gateway() ) {
$crumbs[3] = array( $crumbs[3] = array(
_x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ), _x( 'Change Payment Method', 'the page title of the change payment method form', 'woocommerce-subscriptions' ),
'', '',
); );
} else {
$crumbs[3] = array(
_x( 'Add Payment Method', 'the page title of the add payment method form', 'woocommerce-subscriptions' ),
'',
);
}
} }
return $crumbs; return $crumbs;
@@ -645,6 +812,37 @@ class WC_Subscriptions_Change_Payment_Gateway {
return $needs_payment; return $needs_payment;
} }
/**
* Display a login form on the change payment method page if the customer isn't logged in.
*
* @param string $content The default HTML page content.
* @return string $content.
* @since 2.5.0
*/
public static function maybe_request_log_in( $content ) {
global $wp;
if ( ! self::$is_request_to_change_payment || is_user_logged_in() || ! isset( $wp->query_vars['order-pay'] ) ) {
return $content;
}
$subscription = wcs_get_subscription( absint( $wp->query_vars['order-pay'] ) );
if ( $subscription ) {
wc_add_notice( __( 'Please log in to your account below to choose a new payment method for your subscription.', 'woocommerce-subscriptions' ), 'notice' );
ob_start();
woocommerce_login_form( array(
'redirect' => $subscription->get_change_payment_method_url(),
'message' => wc_print_notices( true ),
) );
$content = ob_get_clean();
}
return $content;
}
/** Deprecated Functions **/ /** Deprecated Functions **/
/** /**

View File

@@ -48,10 +48,10 @@ class WC_Subscriptions_Checkout {
public static function attach_dependant_hooks() { public static function attach_dependant_hooks() {
// Make sure guest checkout is not enabled in option param passed to WC JS // Make sure guest checkout is not enabled in option param passed to WC JS
if ( WC_Subscriptions::is_woocommerce_pre( '3.3' ) ) { if ( WC_Subscriptions::is_woocommerce_pre( '3.3' ) ) {
add_filter( 'woocommerce_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 ); add_filter( 'woocommerce_params', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 1 );
add_filter( 'wc_checkout_params', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 1 ); add_filter( 'wc_checkout_params', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 1 );
} else { } else {
add_filter( 'woocommerce_get_script_data', __CLASS__ . '::filter_woocommerce_script_paramaters', 10, 2 ); add_filter( 'woocommerce_get_script_data', array( __CLASS__, 'filter_woocommerce_script_parameters' ), 10, 2 );
} }
} }
@@ -455,11 +455,18 @@ class WC_Subscriptions_Checkout {
* Also make sure the guest checkout option value passed to the woocommerce.js forces registration. * Also make sure the guest checkout option value passed to the woocommerce.js forces registration.
* Otherwise the registration form is hidden by woocommerce.js. * Otherwise the registration form is hidden by woocommerce.js.
* *
* @since 1.1 * @param string $handle Default empty string ('').
* @param array $woocommerce_params
*
* @since 2.5.3
* @return array
*/ */
public static function filter_woocommerce_script_paramaters( $woocommerce_params, $handle = '' ) { public static function filter_woocommerce_script_parameters( $woocommerce_params, $handle = '' ) {
// WC 3.3+ deprecates handle-specific filters in favor of 'woocommerce_get_script_data'. // WC 3.3+ deprecates handle-specific filters in favor of 'woocommerce_get_script_data'.
if ( 'woocommerce_get_script_data' === current_filter() && ! in_array( $handle, array( 'woocommerce', 'wc-checkout' ) ) ) { if ( 'woocommerce_get_script_data' === current_filter() && ! in_array( $handle, array(
'woocommerce',
'wc-checkout',
) ) ) {
return $woocommerce_params; return $woocommerce_params;
} }
@@ -470,6 +477,19 @@ class WC_Subscriptions_Checkout {
return $woocommerce_params; return $woocommerce_params;
} }
/**
* Also make sure the guest checkout option value passed to the woocommerce.js forces registration.
* Otherwise the registration form is hidden by woocommerce.js.
*
* @since 1.1
* @deprecated 2.5.3
*/
public static function filter_woocommerce_script_paramaters( $woocommerce_params, $handle = '' ) {
wcs_deprecated_function( __METHOD__, '2.5.3', 'WC_Subscriptions_Admin::filter_woocommerce_script_parameters( $woocommerce_params, $handle )' );
return self::filter_woocommerce_script_parameters( $woocommerce_params, $handle );
}
/** /**
* During the checkout process, force registration when the cart contains a subscription. * During the checkout process, force registration when the cart contains a subscription.
* *

View File

@@ -1195,7 +1195,7 @@ class WC_Subscriptions_Manager {
} }
/** /**
* Returns the number of completed payments for a given subscription (including the intial payment). * Returns the number of completed payments for a given subscription (including the initial payment).
* *
* @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key() * @param string $subscription_key A subscription key of the form created by @see self::get_subscription_key()
* @param int $user_id The ID of the user who owns the subscriptions. Although this parameter is optional, if you have the User ID you should pass it to improve performance. * @param int $user_id The ID of the user who owns the subscriptions. Although this parameter is optional, if you have the User ID you should pass it to improve performance.

View File

@@ -409,7 +409,9 @@ class WC_Subscriptions_Product {
$sale_price = self::get_sale_price( $product ); $sale_price = self::get_sale_price( $product );
$active_price = ( $subscription_price ) ? $subscription_price : self::get_regular_price( $product ); $active_price = ( $subscription_price ) ? $subscription_price : self::get_regular_price( $product );
if ( $product->is_on_sale() && $subscription_price > $sale_price ) { // Ensure that $sale_price is non-empty because other plugins can use woocommerce_product_is_on_sale filter to
// forcefully set a product's is_on_sale flag (like Dynamic Pricing )
if ( $product->is_on_sale() && '' !== $sale_price && $subscription_price > $sale_price ) {
$active_price = $sale_price; $active_price = $sale_price;
} }
@@ -778,10 +780,12 @@ class WC_Subscriptions_Product {
*/ */
public static function needs_one_time_shipping( $product ) { public static function needs_one_time_shipping( $product ) {
$product = self::maybe_get_product_instance( $product ); $product = self::maybe_get_product_instance( $product );
$variation = null;
if ( $product && $product->is_type( 'variation' ) && is_callable( array( $product, 'get_parent_id' ) ) ) { if ( $product && $product->is_type( 'variation' ) && is_callable( array( $product, 'get_parent_id' ) ) ) {
$variation = $product;
$product = self::maybe_get_product_instance( $product->get_parent_id() ); $product = self::maybe_get_product_instance( $product->get_parent_id() );
} }
return apply_filters( 'woocommerce_subscriptions_product_needs_one_time_shipping', 'yes' === self::get_meta_data( $product, 'subscription_one_time_shipping', 'no' ), $product ); return apply_filters( 'woocommerce_subscriptions_product_needs_one_time_shipping', 'yes' === self::get_meta_data( $product, 'subscription_one_time_shipping', 'no' ), $product, $variation );
} }
/** /**

View File

@@ -1771,7 +1771,7 @@ class WC_Subscriptions_Switcher {
* @since 2.0 * @since 2.0
*/ */
public static function remove_print_switch_link() { public static function remove_print_switch_link() {
remove_filter( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10 ); remove_action( 'woocommerce_order_item_meta_end', __CLASS__ . '::print_switch_link', 10 );
} }
/** /**
@@ -1780,7 +1780,7 @@ class WC_Subscriptions_Switcher {
* @since 2.0 * @since 2.0
*/ */
public static function add_print_switch_link( $table_content ) { public static function add_print_switch_link( $table_content ) {
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 );
return $table_content; return $table_content;
} }

View File

@@ -919,7 +919,10 @@ class WC_Subscriptions_Synchroniser {
add_filter( 'woocommerce_subscriptions_product_trial_expiration_date', __METHOD__, 10, 2 ); // avoid infinite loop add_filter( 'woocommerce_subscriptions_product_trial_expiration_date', __METHOD__, 10, 2 ); // avoid infinite loop
// 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 // 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 ( $trial_expiration_timestamp > $first_payment_timestamp && gmdate( 'Ymd', $first_payment_timestamp ) == gmdate( 'Ymd', $trial_expiration_timestamp ) ) { // Use site time to check if the trial expiration and first payment fall on the same day
$site_offset = get_option( 'gmt_offset' ) * 3600;
if ( $trial_expiration_timestamp > $first_payment_timestamp && gmdate( 'Ymd', $first_payment_timestamp + $site_offset ) === gmdate( 'Ymd', $trial_expiration_timestamp + $site_offset ) ) {
$trial_expiration_date = date( 'Y-m-d H:i:s', $first_payment_timestamp ); $trial_expiration_date = date( 'Y-m-d H:i:s', $first_payment_timestamp );
} }
} }
@@ -1097,9 +1100,9 @@ class WC_Subscriptions_Synchroniser {
$subscription = wcs_get_subscription( $post_id ); $subscription = wcs_get_subscription( $post_id );
foreach ( $subscription->get_items() as $item ) { foreach ( $subscription->get_items() as $item ) {
$product_id = wcs_get_canonical_product_id( $item ); $product = $item->get_product();
if ( self::is_product_synced( $product_id ) ) { if ( self::is_product_synced( $product ) ) {
update_post_meta( $subscription->get_id(), '_contains_synced_subscription', 'true' ); update_post_meta( $subscription->get_id(), '_contains_synced_subscription', 'true' );
break; break;
} }
@@ -1182,10 +1185,10 @@ class WC_Subscriptions_Synchroniser {
* @since 2.2.3 * @since 2.2.3
*/ */
public static function maybe_add_meta_for_new_line_item( $item_id, $item, $subscription_id ) { public static function maybe_add_meta_for_new_line_item( $item_id, $item, $subscription_id ) {
if ( is_callable( array( $item, 'get_product_id' ) ) ) { if ( is_callable( array( $item, 'get_product' ) ) ) {
$product_id = wcs_get_canonical_product_id( $item ); $product = $item->get_product();
if ( self::is_product_synced( $product_id ) ) { if ( self::is_product_synced( $product ) ) {
self::maybe_add_subscription_meta( $subscription_id ); self::maybe_add_subscription_meta( $subscription_id );
} }
} }

View File

@@ -73,6 +73,8 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
switch ( $new_status ) { switch ( $new_status ) {
case 'active' : case 'active' :
$this->unschedule_actions( 'woocommerce_scheduled_subscription_end_of_prepaid_term', $this->get_action_args( 'end', $subscription ) );
foreach ( $this->action_hooks as $action_hook => $date_type ) { foreach ( $this->action_hooks as $action_hook => $date_type ) {
$event_time = $subscription->get_time( $date_type ); $event_time = $subscription->get_time( $date_type );
@@ -94,6 +96,7 @@ class WCS_Action_Scheduler extends WCS_Scheduler {
as_schedule_single_action( $event_time, $action_hook, $action_args ); as_schedule_single_action( $event_time, $action_hook, $action_args );
} }
} }
break; break;
case 'pending-cancel' : case 'pending-cancel' :

View File

@@ -941,7 +941,15 @@ class WCS_Cart_Renewal {
*/ */
protected function set_cart_hash( $order_id ) { protected function set_cart_hash( $order_id ) {
$order = wc_get_order( $order_id ); $order = wc_get_order( $order_id );
wcs_set_objects_property( $order, 'cart_hash', md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ) );
// Use cart hash generator introduced in WooCommerce 3.6
if ( is_callable( array( WC()->cart, 'get_cart_hash' ) ) ) {
$cart_hash = WC()->cart->get_cart_hash();
} else {
$cart_hash = md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total );
}
wcs_set_objects_property( $order, 'cart_hash', $cart_hash );
} }
/** /**
@@ -1361,8 +1369,10 @@ class WCS_Cart_Renewal {
protected function apply_order_coupon( $order, $coupon ) { protected function apply_order_coupon( $order, $coupon ) {
$coupon_code = $coupon->get_code(); $coupon_code = $coupon->get_code();
// Set order products as the product ids on the coupon // Set order products as the product ids on the coupon if the coupon does not already have usage restrictions for some products
if ( ! $coupon->get_product_ids() ) {
$coupon->set_product_ids( $this->get_products( $order ) ); $coupon->set_product_ids( $this->get_products( $order ) );
}
// Store the coupon info for later // Store the coupon info for later
$this->store_coupon( $order->get_id(), $coupon ); $this->store_coupon( $order->get_id(), $coupon );

View File

@@ -131,13 +131,13 @@ class WCS_Failed_Scheduled_Action_Manager {
) ); ) );
$notice->set_actions( array( $notice->set_actions( array(
array( array(
'name' => __( 'Ignore this error (not recommended)', 'woocommerce-subscriptions' ), 'name' => __( 'Ignore this error', 'woocommerce-subscriptions' ),
'url' => wp_nonce_url( add_query_arg( 'wcs_scheduled_action_timeout_error_notice', 'ignore' ), 'wcs_scheduled_action_timeout_error_notice', '_wcsnonce' ), 'url' => wp_nonce_url( add_query_arg( 'wcs_scheduled_action_timeout_error_notice', 'ignore' ), 'wcs_scheduled_action_timeout_error_notice', '_wcsnonce' ),
'class' => 'button', 'class' => 'button',
), ),
array( array(
'name' => __( 'Open a ticket', 'woocommerce-subscriptions' ), 'name' => __( 'Learn more', 'woocommerce-subscriptions' ),
'url' => 'https://woocommerce.com/my-account/marketplace-ticket-form/', 'url' => 'https://docs.woocommerce.com/document/subscriptions/scheduled-action-errors/',
'class' => 'button button-primary', 'class' => 'button button-primary',
), ),
) ); ) );

View File

@@ -0,0 +1,151 @@
<?php
/**
* Class for managing Auto Renew Toggle on View Subscription page of My Account
*
* @package WooCommerce Subscriptions
* @category Class
* @author Prospress
* @since 2.5.0
*/
class WCS_My_Account_Auto_Renew_Toggle {
/**
* The auto-renewal toggle setting ID.
*
* @var string
*/
protected static $setting_id;
/**
* Initialize filters and hooks for class.
*
* @since 2.5.0
*/
public static function init() {
self::$setting_id = WC_Subscriptions_Admin::$option_prefix . '_enable_auto_renewal_toggle';
add_action( 'wp_ajax_wcs_disable_auto_renew', array( __CLASS__, 'disable_auto_renew' ) );
add_action( 'wp_ajax_wcs_enable_auto_renew', array( __CLASS__, 'enable_auto_renew' ) );
add_filter( 'woocommerce_subscription_settings', array( __CLASS__, 'add_setting' ), 20 );
}
/**
* Check all conditions for whether auto-renewal can be changed is possible
*
* @param WC_Subscription $subscription The subscription for which the checks for auto-renewal needs to be made
* @return boolean
* @since 2.5.0
*/
public static function can_subscription_auto_renewal_be_changed( $subscription ) {
if ( ! self::is_enabled() ) {
return false;
}
// Cannot change to auto-renewal for a subscription with status other than active
if ( ! $subscription->has_status( 'active' ) ) {
return false;
}
// Cannot change to auto-renewal for a subscription with 0 total
if ( 0 == $subscription->get_total() ) { // Not using strict comparison intentionally
return false;
}
// Cannot change to auto-renewal for a subscription in the final billing period. No next renewal date.
if ( 0 == $subscription->get_date( 'next_payment' ) ) { // Not using strict comparison intentionally
return false;
}
// If it is not a manual subscription, and the payment gateway is PayPal Standard
if ( ! $subscription->is_manual() && $subscription->payment_method_supports( 'gateway_scheduled_payments' ) ) {
return false;
}
// Looks like changing to auto-renewal is indeed possible
return true;
}
/**
* Disable auto renewal of subscription
*
* @since 2.5.0
*/
public static function disable_auto_renew() {
if ( ! isset( $_POST['subscription_id'] ) ) {
return -1;
}
$subscription_id = absint( $_POST['subscription_id'] );
check_ajax_referer( "toggle-auto-renew-{$subscription_id}", 'security' );
$subscription = wcs_get_subscription( $subscription_id );
if ( $subscription ) {
$subscription->set_requires_manual_renewal( true );
$subscription->save();
self::send_ajax_response( $subscription );
}
}
/**
* Enable auto renewal of subscription
*
* @since 2.5.0
*/
public static function enable_auto_renew() {
if ( ! isset( $_POST['subscription_id'] ) ) {
return -1;
}
$subscription_id = absint( $_POST['subscription_id'] );
check_ajax_referer( "toggle-auto-renew-{$subscription_id}", 'security' );
$subscription = wcs_get_subscription( $subscription_id );
if ( wc_get_payment_gateway_by_order( $subscription ) ) {
$subscription->set_requires_manual_renewal( false );
$subscription->save();
self::send_ajax_response( $subscription );
}
}
/**
* Send a response after processing the AJAX request so the page can be updated.
*
* @param WC_Subscription $subscription
*/
protected static function send_ajax_response( $subscription ) {
wp_send_json( array( 'payment_method' => esc_attr( $subscription->get_payment_method_to_display( 'customer' ) ) ) );
}
/**
* Add a setting to allow store managers to enable or disable the auto-renewal toggle.
*
* @param array $settings
* @return array
* @since 2.5.0
*/
public static function add_setting( $settings ) {
WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_subscriptions_turn_off_automatic_payments', array(
'id' => self::$setting_id,
'name' => __( 'Auto Renewal Toggle', 'woocommerce-subscriptions' ),
'desc' => __( 'Display the auto renewal toggle', 'woocommerce-subscriptions' ),
'desc_tip' => __( 'Allow customers to turn on and off automatic renewals from their View Subscription page.', 'woocommerce-subscriptions' ),
'default' => 'no',
'type' => 'checkbox',
) );
return $settings;
}
/**
* Checks if the store has enabled the auto-renewal toggle.
*
* @return bool true if the toggle is enabled, otherwise false.
* @since 2.5.0
*/
public static function is_enabled() {
return 'yes' === get_option( self::$setting_id, 'no' );
}
}

View File

@@ -9,9 +9,6 @@
*/ */
class WCS_My_Account_Payment_Methods { class WCS_My_Account_Payment_Methods {
/* A cache of a customer's payment tokens to avoid running multiple queries in the same request */
protected static $customer_tokens = array();
/** /**
* Initialize filters and hooks for class. * Initialize filters and hooks for class.
* *
@@ -27,6 +24,7 @@ class WCS_My_Account_Payment_Methods {
add_action( 'woocommerce_payment_token_deleted',array( __CLASS__, 'maybe_update_subscriptions_payment_meta' ), 10, 2 ); add_action( 'woocommerce_payment_token_deleted',array( __CLASS__, 'maybe_update_subscriptions_payment_meta' ), 10, 2 );
add_action( 'woocommerce_payment_token_set_default', array( __CLASS__, 'display_default_payment_token_change_notice' ), 10, 2 ); add_action( 'woocommerce_payment_token_set_default', array( __CLASS__, 'display_default_payment_token_change_notice' ), 10, 2 );
add_action( 'wp', array( __CLASS__, 'update_subscription_tokens' ) ); add_action( 'wp', array( __CLASS__, 'update_subscription_tokens' ) );
} }
/** /**
@@ -41,8 +39,8 @@ class WCS_My_Account_Payment_Methods {
if ( $payment_token instanceof WC_Payment_Token && isset( $payment_token_data['actions']['delete']['url'] ) ) { if ( $payment_token instanceof WC_Payment_Token && isset( $payment_token_data['actions']['delete']['url'] ) ) {
if ( 0 < count( self::get_subscriptions_by_token( $payment_token ) ) ) { if ( 0 < count( WCS_Payment_Tokens::get_subscriptions_from_token( $payment_token ) ) ) {
if ( self::customer_has_alternative_token( $payment_token ) ) { if ( WCS_Payment_Tokens::customer_has_alternative_token( $payment_token ) ) {
$delete_subscription_token_args = array( $delete_subscription_token_args = array(
'delete_subscription_token' => $payment_token->get_id(), 'delete_subscription_token' => $payment_token->get_id(),
'wcs_nonce' => wp_create_nonce( 'delete_subscription_token_' . $payment_token->get_id() ), 'wcs_nonce' => wp_create_nonce( 'delete_subscription_token_' . $payment_token->get_id() ),
@@ -76,7 +74,7 @@ class WCS_My_Account_Payment_Methods {
// init payment gateways // init payment gateways
WC()->payment_gateways(); WC()->payment_gateways();
$new_token = self::get_customers_alternative_token( $deleted_token ); $new_token = WCS_Payment_Tokens::get_customers_alternative_token( $deleted_token );
if ( empty( $new_token ) ) { if ( empty( $new_token ) ) {
$notice = esc_html__( 'The deleted payment method was used for automatic subscription payments, we couldn\'t find an alternative token payment method token to change your subscriptions to.', 'woocommerce-subscriptions' ); $notice = esc_html__( 'The deleted payment method was used for automatic subscription payments, we couldn\'t find an alternative token payment method token to change your subscriptions to.', 'woocommerce-subscriptions' );
@@ -84,20 +82,18 @@ class WCS_My_Account_Payment_Methods {
return; return;
} }
$subscriptions = self::get_subscriptions_by_token( $deleted_token ); $subscriptions = WCS_Payment_Tokens::get_subscriptions_from_token( $deleted_token );
if ( empty( $subscriptions ) ) { if ( empty( $subscriptions ) ) {
return; return;
} }
foreach ( $subscriptions as $subscription ) { foreach ( $subscriptions as $subscription ) {
$subscription = wcs_get_subscription( $subscription );
if ( empty( $subscription ) ) { if ( empty( $subscription ) ) {
continue; continue;
} }
if ( self::update_subscription_token( $subscription, $new_token, $deleted_token ) ) { if ( WCS_Payment_Tokens::update_subscription_token( $subscription, $new_token, $deleted_token ) ) {
$subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer deleted a token from their My Account page. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $deleted_token->get_token(), $new_token->get_token() ) ); $subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer deleted a token from their My Account page. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $deleted_token->get_token(), $new_token->get_token() ) );
} }
} }
@@ -112,75 +108,6 @@ class WCS_My_Account_Payment_Methods {
wc_add_notice( $notice , 'notice' ); wc_add_notice( $notice , 'notice' );
} }
/**
* Update the subscription payment meta to change from an old payment token to a new one.
*
* @param WC_Subscription $subscription The subscription to update.
* @param WC_Payment_Token $new_token The new payment token.
* @param WC_Payment_Token $old_token The old payment token.
* @return bool Whether the subscription was updated or not.
*/
protected static function update_subscription_token( $subscription, $new_token, $old_token ) {
$payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
$token_payment_gateway = $old_token->get_gateway_id();
$token_meta_key = '';
// Attempt to find the token meta key from the subscription payment meta and the old token.
if ( is_array( $payment_method_meta ) && isset( $payment_method_meta[ $token_payment_gateway ] ) && is_array( $payment_method_meta[ $token_payment_gateway ] ) ) {
foreach ( $payment_method_meta[ $token_payment_gateway ] as $meta_table => $meta ) {
foreach ( $meta as $meta_key => $meta_data ) {
if ( $old_token->get_token() === $meta_data['value'] ) {
$token_meta_key = $meta_key;
break 2;
}
}
}
}
$updated = update_post_meta( $subscription->get_id(), $token_meta_key, $new_token->get_token(), $old_token->get_token() );
if ( $updated ) {
do_action( 'woocommerce_subscription_token_changed', $subscription, $new_token, $old_token );
}
return $updated;
}
/**
* Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method,
* customer id and token value stored in post meta will be returned.
*
* @param WC_Payment_Token payment token object
* @return array subscription posts
* @since 2.2.7
*/
public static function get_subscriptions_by_token( $payment_token ) {
$meta_query = array(
array(
'key' => '_payment_method',
'value' => $payment_token->get_gateway_id(),
),
array(
'key' => '_requires_manual_renewal',
'value' => 'false',
),
array(
'value' => $payment_token->get_token(),
),
);
$user_subscriptions = get_posts( array(
'post_type' => 'shop_subscription',
'post_status' => array( 'wc-pending', 'wc-active', 'wc-on-hold' ),
'meta_query' => $meta_query,
'posts_per_page' => -1,
'post__in' => WCS_Customer_Store::instance()->get_users_subscription_ids( $payment_token->get_user_id() ),
) );
return apply_filters( 'woocommerce_subscriptions_by_payment_token', $user_subscriptions, $payment_token );
}
/** /**
* Get a WC_Payment_Token label. eg Visa ending in 1234 * Get a WC_Payment_Token label. eg Visa ending in 1234
* *
@@ -199,66 +126,6 @@ class WCS_My_Account_Payment_Methods {
return $label; return $label;
} }
/**
* Get a list of customer payment tokens. Caches results to avoid multiple database queries per request
*
* @param string (optional) Gateway ID for getting tokens for a specific gateway.
* @param int (optional) The customer id - defaults to the current user.
* @return array of WC_Payment_Token objects
* @since 2.2.7
*/
public static function get_customer_tokens( $gateway_id = '', $customer_id = '' ) {
if ( '' === $customer_id ) {
$customer_id = get_current_user_id();
}
if ( ! isset( self::$customer_tokens[ $customer_id ][ $gateway_id ] ) ) {
self::$customer_tokens[ $customer_id ][ $gateway_id ] = WC_Payment_Tokens::get_customer_tokens( $customer_id, $gateway_id );
}
return self::$customer_tokens[ $customer_id ][ $gateway_id ];
}
/**
* Get the customer's alternative token.
*
* @param WC_Payment_Token $token The token to find an alternative for
* @return WC_Payment_Token The customer's alternative token
* @since 2.2.7
*/
public static function get_customers_alternative_token( $token ) {
$payment_tokens = self::get_customer_tokens( $token->get_gateway_id(), $token->get_user_id() );
$alternative_token = null;
// Remove the token we're trying to find an alternative for.
unset( $payment_tokens[ $token->get_id() ] );
if ( count( $payment_tokens ) === 1 ) {
$alternative_token = reset( $payment_tokens );
} else {
foreach ( $payment_tokens as $payment_token ) {
// If there is a default token we can use it as an alternative.
if ( $payment_token->is_default() ) {
$alternative_token = $payment_token;
break;
}
}
}
return $alternative_token;
}
/**
* Determine if the customer has an alternative token.
*
* @param WC_Payment_Token payment token object
* @return bool
* @since 2.2.7
*/
public static function customer_has_alternative_token( $token ) {
return self::get_customers_alternative_token( $token ) !== null;
}
/** /**
* Display a notice when a customer sets a new default token notifying them of what this means for their subscriptions. * Display a notice when a customer sets a new default token notifying them of what this means for their subscriptions.
* *
@@ -268,12 +135,12 @@ class WCS_My_Account_Payment_Methods {
*/ */
public static function display_default_payment_token_change_notice( $default_token_id, $default_token ) { public static function display_default_payment_token_change_notice( $default_token_id, $default_token ) {
$display_notice = false; $display_notice = false;
$customer_tokens = self::get_customer_tokens( $default_token->get_gateway_id(), $default_token->get_user_id() ); $customer_tokens = WCS_Payment_Tokens::get_customer_tokens( $default_token->get_user_id(), $default_token->get_gateway_id() );
unset( $customer_tokens[ $default_token_id ] ); unset( $customer_tokens[ $default_token_id ] );
// Check if there are subscriptions for one of the customer's other tokens. // Check if there are subscriptions for one of the customer's other tokens.
foreach ( $customer_tokens as $token ) { foreach ( $customer_tokens as $token ) {
if ( count( self::get_subscriptions_by_token( $token ) ) > 0 ) { if ( count( WCS_Payment_Tokens::get_subscriptions_from_token( $token ) ) > 0 ) {
$display_notice = true; $display_notice = true;
break; break;
} }
@@ -317,14 +184,12 @@ class WCS_My_Account_Payment_Methods {
return; return;
} }
$tokens = self::get_customer_tokens( $default_token->get_gateway_id(), $default_token->get_user_id() ); $tokens = WCS_Payment_Tokens::get_customer_tokens( $default_token->get_user_id(), $default_token->get_gateway_id() );
unset( $tokens[ $default_token_id ] ); unset( $tokens[ $default_token_id ] );
foreach ( $tokens as $old_token ) { foreach ( $tokens as $old_token ) {
foreach ( self::get_subscriptions_by_token( $old_token ) as $subscription ) { foreach ( WCS_Payment_Tokens::get_subscriptions_from_token( $old_token ) as $subscription ) {
$subscription = wcs_get_subscription( $subscription ); if ( ! empty( $subscription ) && WCS_Payment_Tokens::update_subscription_token( $subscription, $default_token, $old_token ) ) {
if ( ! empty( $subscription ) && self::update_subscription_token( $subscription, $default_token, $old_token ) ) {
$subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer changed their default token and opted to update their subscriptions. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $old_token->get_token(), $default_token->get_token() ) ); $subscription->add_order_note( sprintf( _x( 'Payment method meta updated after customer changed their default token and opted to update their subscriptions. Payment meta changed from %1$s to %2$s', 'used in subscription note', 'woocommerce-subscriptions' ), $old_token->get_token(), $default_token->get_token() ) );
} }
} }
@@ -333,4 +198,49 @@ class WCS_My_Account_Payment_Methods {
wp_redirect( remove_query_arg( array( 'update-subscription-tokens', 'token-id', '_wcsnonce' ) ) ); wp_redirect( remove_query_arg( array( 'update-subscription-tokens', 'token-id', '_wcsnonce' ) ) );
exit(); exit();
} }
/**
* Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method,
* customer id and token value stored in post meta will be returned.
*
* @since 2.2.7
* @deprecated 2.5.0
*/
public static function get_subscriptions_by_token( $payment_token ) {
_deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_subscriptions_from_token()' );
return WCS_Payment_Tokens::get_subscriptions_from_token( $payment_token );
}
/**
* Get a list of customer payment tokens. Caches results to avoid multiple database queries per request
*
* @since 2.2.7
* @deprecated 2.5.0
*/
public static function get_customer_tokens( $gateway_id = '', $customer_id = '' ) {
_deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_customer_tokens()' );
return WCS_Payment_Tokens::get_customer_tokens( $customer_id, $gateway_id );
}
/**
* Get the customer's alternative token.
*
* @since 2.2.7
* @deprecated 2.5.0
*/
public static function get_customers_alternative_token( $token ) {
_deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::get_customers_alternative_token()' );
return WCS_Payment_Tokens::get_customers_alternative_token( $token );
}
/**
* Determine if the customer has an alternative token.
*
* @since 2.2.7
* @deprecated 2.5.0
*/
public static function customer_has_alternative_token( $token ) {
_deprecated_function( __METHOD__, '2.5.0', 'WCS_Payment_Tokens::customer_has_alternative_token()' );
return WCS_Payment_Tokens::customer_has_alternative_token( $token );
}
} }

View File

@@ -0,0 +1,180 @@
<?php
/**
* WooCommerce Subscriptions Payment Tokens
*
* An API for storing and managing tokens for subscriptions.
*
* @package WooCommerce Subscriptions
* @category Class
* @author Prospress
* @since 2.5.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Payment_Tokens extends WC_Payment_Tokens {
// A cache of a customer's payment tokens to avoid running multiple queries in the same request.
protected static $customer_tokens = array();
/**
* Update the subscription payment meta to change from an old payment token to a new one.
*
* @param WC_Subscription $subscription The subscription to update.
* @param WC_Payment_Token $new_token The new payment token.
* @param WC_Payment_Token $old_token The old payment token.
* @return bool Whether the subscription was updated or not.
*/
public static function update_subscription_token( $subscription, $new_token, $old_token ) {
$token_payment_gateway = $old_token->get_gateway_id();
$payment_meta_table = self::get_subscription_payment_meta( $subscription, $token_payment_gateway );
$token_meta_key = '';
// Attempt to find the token meta key from the subscription payment meta and the old token.
if ( is_array( $payment_meta_table ) ) {
foreach ( $payment_meta_table as $meta_table => $meta ) {
foreach ( $meta as $meta_key => $meta_data ) {
if ( $old_token->get_token() === $meta_data['value'] ) {
$token_meta_key = $meta_key;
break 2;
}
}
}
}
$updated = update_post_meta( $subscription->get_id(), $token_meta_key, $new_token->get_token(), $old_token->get_token() );
if ( $updated ) {
do_action( 'woocommerce_subscription_token_changed', $subscription, $new_token, $old_token );
}
return $updated;
}
/**
* Get all payment meta on a subscription for a gateway.
*
* @param WC_Subscription $subscription The subscription to update.
* @param string $gateway_id The target gateway ID.
* @return bool|array Payment meta data. False if no meta is found.
* @since 2.5.0
*/
public static function get_subscription_payment_meta( $subscription, $gateway_id ) {
$payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
if ( is_array( $payment_method_meta ) && isset( $payment_method_meta[ $gateway_id ] ) && is_array( $payment_method_meta[ $gateway_id ] ) ) {
return $payment_method_meta[ $gateway_id ];
}
return false;
}
/**
* Get subscriptions by a WC_Payment_Token. All automatic subscriptions with the token's payment method,
* customer id and token value stored in post meta will be returned.
*
* @param WC_Payment_Token $payment_token Payment token object.
* @return array subscription posts
* @since 2.5.0
*/
public static function get_subscriptions_from_token( $payment_token ) {
$meta_query = array(
array(
'key' => '_payment_method',
'value' => $payment_token->get_gateway_id(),
),
array(
'key' => '_requires_manual_renewal',
'value' => 'false',
),
array(
'value' => $payment_token->get_token(),
),
);
$subscription_ids = get_posts( array(
'post_type' => 'shop_subscription',
'post_status' => array( 'wc-pending', 'wc-active', 'wc-on-hold' ),
'meta_query' => $meta_query,
'posts_per_page' => -1,
'fields' => 'ids',
'post__in' => WCS_Customer_Store::instance()->get_users_subscription_ids( $payment_token->get_user_id() ),
) );
if ( has_filter( 'woocommerce_subscriptions_by_payment_token' ) ) {
wcs_deprecated_function( 'The "woocommerce_subscriptions_by_payment_token" hook should no longer be used. It previously filtered post objects and in moving to CRUD and Subscription APIs the "woocommerce_subscriptions_by_payment_token"', '2.5.0', 'woocommerce_subscriptions_from_payment_token' );
$subscription_posts = apply_filters( 'woocommerce_subscriptions_by_payment_token', array_map( 'get_post', $subscription_ids ), $payment_token );
$subscription_ids = array_unique( array_merge( $subscription_ids, wp_list_pluck( $subscription_posts, 'ID' ) ) );
}
$user_subscriptions = array();
foreach ( $subscription_ids as $subscription_id ) {
$user_subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id );
}
return apply_filters( 'woocommerce_subscriptions_from_payment_token', $user_subscriptions, $payment_token );
}
/**
* Get a list of customer payment tokens. Caches results to avoid multiple database queries per request
*
* @param int (optional) The customer id - defaults to the current user.
* @param string (optional) Gateway ID for getting tokens for a specific gateway.
* @return array of WC_Payment_Token objects.
* @since 2.2.7
*/
public static function get_customer_tokens( $customer_id = '', $gateway_id = '' ) {
if ( '' === $customer_id ) {
$customer_id = get_current_user_id();
}
if ( ! isset( self::$customer_tokens[ $customer_id ][ $gateway_id ] ) ) {
self::$customer_tokens[ $customer_id ][ $gateway_id ] = parent::get_customer_tokens( $customer_id, $gateway_id );
}
return self::$customer_tokens[ $customer_id ][ $gateway_id ];
}
/**
* Get the customer's alternative token.
*
* @param WC_Payment_Token $token The token to find an alternative for.
* @return WC_Payment_Token The customer's alternative token.
* @since 2.2.7
*/
public static function get_customers_alternative_token( $token ) {
$payment_tokens = self::get_customer_tokens( $token->get_gateway_id(), $token->get_user_id() );
$alternative_token = null;
// Remove the token we're trying to find an alternative for.
unset( $payment_tokens[ $token->get_id() ] );
if ( count( $payment_tokens ) === 1 ) {
$alternative_token = reset( $payment_tokens );
} else {
foreach ( $payment_tokens as $payment_token ) {
// If there is a default token we can use it as an alternative.
if ( $payment_token->is_default() ) {
$alternative_token = $payment_token;
break;
}
}
}
return $alternative_token;
}
/**
* Determine if the customer has an alternative token.
*
* @param WC_Payment_Token $token Payment token object.
* @return bool
* @since 2.2.7
*/
public static function customer_has_alternative_token( $token ) {
return self::get_customers_alternative_token( $token ) !== null;
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* WooCommerce Subscriptions Permalink Manager
*
* Handles and allows WCS related permalinks/endpoints.
*
* @package WooCommerce Subscriptions
* @category Class
* @author Prospress
* @since 2.5.3
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class WCS_Permalink_Manager
*/
class WCS_Permalink_Manager {
/**
* If the notice has been trigger, set to true to avoid duplicate notices.
*
* @var bool
* @since 2.5.3
*/
protected static $notice_triggered = false;
/**
* The options saved in DB related to permalinks.
*
* @var array
* @since 2.5.3
*/
protected static $permalink_options = array(
'woocommerce_myaccount_subscriptions_endpoint',
'woocommerce_myaccount_view_subscription_endpoint',
'woocommerce_myaccount_subscription_payment_method_endpoint',
);
/**
* Hooks.
*
* @since 2.5.3
*/
public static function init() {
add_filter( 'pre_update_option', array( __CLASS__, 'maybe_allow_permalink_update' ), 10, 3 );
}
/**
* Validates that we're not passing the same endpoint.
*
* @param mixed $value The new desired value.
* @param string $option The option being updated.
* @param mixed $old_value The previous option value.
*
* @return mixed
* @since 2.5.3
*/
public static function maybe_allow_permalink_update( $value, $option, $old_value ) {
// If is updating a permalink option.
if ( isset( $_POST[ $option ] ) && in_array( $option, self::$permalink_options, true ) ) { // @codingStandardsIgnoreLine WordPress.CSRF.NonceVerification.NoNonceVerification
foreach ( self::$permalink_options as $permalink_option ) {
if ( $permalink_option === $option ) {
continue;
}
if ( isset( $_POST[ $permalink_option ] ) && $value === $_POST[ $permalink_option ] ) { // @codingStandardsIgnoreLine WordPress.CSRF.NonceVerification.NoNonceVerification
self::show_duplicate_permalink_notice();
return $old_value;
}
}
}
return $value;
}
/**
* Display a warning informing that the endpoints changes has been ignored.
*
* @since 2.5.3
*/
protected static function show_duplicate_permalink_notice() {
if ( ! self::$notice_triggered ) {
self::$notice_triggered = true;
$notice = new WCS_Admin_Notice( 'error' );
$notice->set_simple_content(
// translators: 1$-2$: opening and closing <strong> tags.
sprintf( esc_html__( 'Error saving Subscriptions endpoints: %1$sSubscriptions%2$s, %1$sView subscription%2$s and %1$sSubscription payment method%2$s cannot be the same. The changes have been reverted.', 'woocommerce-subscriptions' ), '<strong>', '</strong>' )
);
$notice->display();
}
}
}

View File

@@ -17,12 +17,18 @@ class WCS_Query extends WC_Query {
add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
add_action( 'parse_request', array( $this, 'parse_request' ), 0 ); add_action( 'parse_request', array( $this, 'parse_request' ), 0 );
add_filter( 'woocommerce_get_breadcrumb', array( $this, 'add_breadcrumb' ), 10 ); add_filter( 'woocommerce_get_breadcrumb', array( $this, 'add_breadcrumb' ), 10 );
add_action( 'pre_get_posts', array( $this, 'maybe_redirect_payment_methods' ) );
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 11 ); add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 11 );
add_filter( 'woocommerce_get_query_vars', array( $this, 'add_wcs_query_vars' ) ); add_filter( 'woocommerce_get_query_vars', array( $this, 'add_wcs_query_vars' ) );
// Inserting your new tab/page into the My Account page. // Inserting your new tab/page into the My Account page.
add_filter( 'woocommerce_account_menu_items', array( $this, 'add_menu_items' ) ); add_filter( 'woocommerce_account_menu_items', array( $this, 'add_menu_items' ) );
// Since WC 3.3.0, add_wcs_query_vars() is enough for custom endpoints to work.
if ( WC_Subscriptions::is_woocommerce_pre( '3.3.0' ) ) {
add_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ), 10, 4 ); add_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ), 10, 4 );
}
add_filter( 'woocommerce_get_endpoint_url', array( $this, 'maybe_redirect_to_only_subscription' ), 10, 2 ); add_filter( 'woocommerce_get_endpoint_url', array( $this, 'maybe_redirect_to_only_subscription' ), 10, 2 );
add_action( 'woocommerce_account_subscriptions_endpoint', array( $this, 'endpoint_content' ) ); add_action( 'woocommerce_account_subscriptions_endpoint', array( $this, 'endpoint_content' ) );
} }
@@ -47,6 +53,7 @@ class WCS_Query extends WC_Query {
); );
if ( ! WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) { if ( ! WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
$this->query_vars['subscriptions'] = get_option( 'woocommerce_myaccount_subscriptions_endpoint', 'subscriptions' ); $this->query_vars['subscriptions'] = get_option( 'woocommerce_myaccount_subscriptions_endpoint', 'subscriptions' );
$this->query_vars['subscription-payment-method'] = get_option( 'woocommerce_myaccount_subscription_payment_method_endpoint', 'subscription-payment-method' );
} }
} }
@@ -123,6 +130,12 @@ class WCS_Query extends WC_Query {
* @return array * @return array
*/ */
public function add_menu_items( $menu_items ) { public function add_menu_items( $menu_items ) {
// If the Subscriptions endpoint setting is empty, don't display it in line with core WC behaviour.
if ( empty( $this->query_vars['subscriptions'] ) ) {
return $menu_items;
}
if ( 1 == count( wcs_get_users_subscriptions() ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) { if ( 1 == count( wcs_get_users_subscriptions() ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) {
$label = __( 'My Subscription', 'woocommerce-subscriptions' ); $label = __( 'My Subscription', 'woocommerce-subscriptions' );
} else { } else {
@@ -148,7 +161,7 @@ class WCS_Query extends WC_Query {
* @return string * @return string
*/ */
public function maybe_redirect_to_only_subscription( $url, $endpoint ) { public function maybe_redirect_to_only_subscription( $url, $endpoint ) {
if ( 'subscriptions' == $endpoint && is_account_page() ) { if ( $this->query_vars['subscriptions'] === $endpoint && is_account_page() ) {
$subscriptions = wcs_get_users_subscriptions(); $subscriptions = wcs_get_users_subscriptions();
if ( is_array( $subscriptions ) && 1 == count( $subscriptions ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) { if ( is_array( $subscriptions ) && 1 == count( $subscriptions ) && apply_filters( 'wcs_my_account_redirect_to_single_subscription', true ) ) {
@@ -216,6 +229,41 @@ class WCS_Query extends WC_Query {
} }
} }
/**
* Redirect to order-pay flow for Subscription Payment Method endpoint.
*
* @param WP_Query $query WordPress query object
* @since 2.5.0
*/
public function maybe_redirect_payment_methods( $query ) {
if ( ! $query->is_main_query() || ! absint( $query->get( 'subscription-payment-method' ) ) ) {
return;
}
$subscription = wcs_get_subscription( absint( $query->get( 'subscription-payment-method' ) ) );
if ( ! $subscription ) {
return;
}
if ( ! $subscription->can_be_updated_to( 'new-payment-method' ) ) {
$url = $subscription->get_view_order_url();
wc_add_notice( __( 'The payment method can not be changed for that subscription.', 'woocommerce-subscriptions' ), 'error' );
} else {
$args = array(
'change_payment_method' => $subscription->get_id(),
'_wpnonce' => wp_create_nonce(),
);
$url = add_query_arg( $args, $subscription->get_checkout_payment_url() );
}
wp_redirect( $url );
exit();
}
/** /**
* Reset the woocommerce_myaccount_view_subscriptions_endpoint option name to woocommerce_myaccount_view_subscription_endpoint * Reset the woocommerce_myaccount_view_subscriptions_endpoint option name to woocommerce_myaccount_view_subscription_endpoint
* *
@@ -262,8 +310,16 @@ class WCS_Query extends WC_Query {
'desc_tip' => true, 'desc_tip' => true,
); );
WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_myaccount_view_order_endpoint', array( $subscriptions_endpoint_setting, $view_subscription_endpoint_setting ), 'multiple_settings' ); $subscription_payment_method_endpoint_setting = array(
'title' => __( 'Subscription payment method', 'woocommerce-subscriptions' ),
'desc' => __( 'Endpoint for the My Account &rarr; Change Subscription Payment Method page', 'woocommerce-subscriptions' ),
'id' => 'woocommerce_myaccount_subscription_payment_method_endpoint',
'type' => 'text',
'default' => 'subscription-payment-method',
'desc_tip' => true,
);
WC_Subscriptions_Admin::insert_setting_after( $settings, 'woocommerce_myaccount_view_order_endpoint', array( $subscriptions_endpoint_setting, $view_subscription_endpoint_setting, $subscription_payment_method_endpoint_setting ), 'multiple_settings' );
return $settings; return $settings;
} }
@@ -279,7 +335,7 @@ class WCS_Query extends WC_Query {
* @return string $url * @return string $url
*/ */
public function get_endpoint_url( $url, $endpoint, $value = '', $permalink = '') { public function get_endpoint_url( $url, $endpoint, $value = '', $permalink = '' ) {
if ( ! empty( $this->query_vars[ $endpoint ] ) ) { if ( ! empty( $this->query_vars[ $endpoint ] ) ) {
remove_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ) ); remove_filter( 'woocommerce_get_endpoint_url', array( $this, 'get_endpoint_url' ) );

View File

@@ -63,7 +63,8 @@ class WCS_Retry_Manager {
add_action( 'woocommerce_subscriptions_retry_status_updated', __CLASS__ . '::maybe_delete_payment_retry_date', 0, 2 ); add_action( 'woocommerce_subscriptions_retry_status_updated', __CLASS__ . '::maybe_delete_payment_retry_date', 0, 2 );
add_action( 'woocommerce_subscription_renewal_payment_failed', __CLASS__ . '::maybe_apply_retry_rule', 10, 2 ); add_action( 'woocommerce_subscription_renewal_payment_failed', array( __CLASS__, 'maybe_apply_retry_rule' ), 10, 2 );
add_action( 'woocommerce_subscription_renewal_payment_failed', array( __CLASS__, 'maybe_reapply_last_retry_rule' ), 15, 2 );
add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' ); add_action( 'woocommerce_scheduled_subscription_payment_retry', __CLASS__ . '::maybe_retry_payment' );
@@ -196,13 +197,13 @@ class WCS_Retry_Manager {
/** /**
* When a payment fails, apply a retry rule, if one exists that applies to this failure. * When a payment fails, apply a retry rule, if one exists that applies to this failure.
* *
* @param WC_Subscription The subscription on which the payment failed * @param WC_Subscription $subscription The subscription on which the payment failed.
* @param WC_Order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param) * @param WC_Order $last_order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param).
*
* @since 2.1 * @since 2.1
*/ */
public static function maybe_apply_retry_rule( $subscription, $last_order ) { public static function maybe_apply_retry_rule( $subscription, $last_order ) {
if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) || ! self::is_scheduled_payment_attempt() ) {
if ( $subscription->is_manual() || ! $subscription->payment_method_supports( 'subscription_date_changes' ) ) {
return; return;
} }
@@ -231,7 +232,7 @@ class WCS_Retry_Manager {
} }
if ( $retry_rule->get_retry_interval() > 0 ) { if ( $retry_rule->get_retry_interval() > 0 ) {
// by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action // by calling this after changing the status, this will also schedule the 'woocommerce_scheduled_subscription_payment_retry' action.
$subscription->update_dates( array( 'payment_retry' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval( $retry_count ) ) ) ); $subscription->update_dates( array( 'payment_retry' => gmdate( 'Y-m-d H:i:s', gmdate( 'U' ) + $retry_rule->get_retry_interval( $retry_count ) ) ) );
} }
@@ -239,6 +240,36 @@ class WCS_Retry_Manager {
} }
} }
/**
* (Maybe) reapply last retry rule if:
* - Payment is no-scheduled
* - $last_order contains a Retry
* - Retry contains a rule
*
* @param WC_Subscription $subscription The subscription on which the payment failed.
* @param WC_Order $last_order The order on which the payment failed (will be the most recent order on the subscription specified with the subscription param).
*
* @since 2.5.0
*/
public static function maybe_reapply_last_retry_rule( $subscription, $last_order ) {
// We're only interested in non-automatic payment attempts.
if ( self::is_scheduled_payment_attempt() ) {
return;
}
$last_retry = self::store()->get_last_retry_for_order( $last_order->get_id() );
if ( ! $last_retry || 'pending' !== $last_retry->get_status() || null === ( $last_retry_rule = $last_retry->get_rule() ) ) {
return;
}
foreach ( array( 'order' => $last_order, 'subscription' => $subscription ) as $object_type => $object ) {
$new_status = $last_retry_rule->get_status_to_apply( $object_type );
if ( '' !== $new_status && ! $object->has_status( $new_status ) ) {
$object->update_status( $new_status, _x( 'Retry rule reapplied:', 'used in order note as reason for why status changed', 'woocommerce-subscriptions' ) );
}
}
}
/** /**
* When a retry hook is triggered, check if the rules for that retry are still valid * When a retry hook is triggered, check if the rules for that retry are still valid
* and if so, retry the payment. * and if so, retry the payment.
@@ -391,9 +422,27 @@ class WCS_Retry_Manager {
} }
} }
/**
* Is `woocommerce_scheduled_subscription_payment` or `woocommerce_scheduled_subscription_payment_retry` current action?
*
* @return boolean
*
* @since 2.5.0
*/
protected static function is_scheduled_payment_attempt() {
/**
* Filter 'Is scheduled payment attempt?'
*
* @param boolean doing_action( 'woocommerce_scheduled_subscription_payment' ) || doing_action( 'woocommerce_scheduled_subscription_payment_retry' )
* @since 2.5.0
*/
return (bool) apply_filters( 'wcs_is_scheduled_payment_attempt', doing_action( 'woocommerce_scheduled_subscription_payment' ) || doing_action( 'woocommerce_scheduled_subscription_payment_retry' ) );
}
/** /**
* Access the object used to interface with the store. * Access the object used to interface with the store.
* *
* @return WCS_Retry_Store
* @since 2.4 * @since 2.4
*/ */
public static function store() { public static function store() {

View File

@@ -8,39 +8,27 @@
class WCS_Template_Loader { class WCS_Template_Loader {
public static function init() { public static function init() {
add_filter( 'wc_get_template', array( __CLASS__, 'add_view_subscription_template' ), 10, 5 );
add_action( 'woocommerce_account_view-subscription_endpoint', array( __CLASS__, 'get_view_subscription_template' ) ); add_action( 'woocommerce_account_view-subscription_endpoint', array( __CLASS__, 'get_view_subscription_template' ) );
add_action( 'woocommerce_subscription_details_table', array( __CLASS__, 'get_subscription_details_template' ) ); add_action( 'woocommerce_subscription_details_table', array( __CLASS__, 'get_subscription_details_template' ) );
add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_subscription_totals_template' ) ); add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_subscription_totals_template' ) );
add_action( 'woocommerce_subscription_totals_table', array( __CLASS__, 'get_order_downloads_template' ), 20 );
} }
/** /**
* Show the subscription template when view a subscription instead of loading the default order template. * Get the view subscription template.
*
* @param $located
* @param $template_name
* @param $args
* @param $template_path
* @param $default_path
* @since 2.0
*/
public static function add_view_subscription_template( $located, $template_name, $args, $template_path, $default_path ) {
global $wp;
if ( 'myaccount/my-account.php' == $template_name && ! empty( $wp->query_vars['view-subscription'] ) && WC_Subscriptions::is_woocommerce_pre( '2.6' ) ) {
$located = wc_locate_template( 'myaccount/view-subscription.php', $template_path, plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
}
return $located;
}
/**
* Get the view subscription template. A post WC v2.6 compatible version of @see WCS_Template_Loader::add_view_subscription_template()
* *
* @param int $subscription_id Subscription ID.
* @since 2.0.17 * @since 2.0.17
*/ */
public static function get_view_subscription_template() { public static function get_view_subscription_template( $subscription_id ) {
wc_get_template( 'myaccount/view-subscription.php', array(), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); $subscription = wcs_get_subscription( absint( $subscription_id ) );
if ( ! $subscription || ! current_user_can( 'view_order', $subscription->get_id() ) ) {
echo '<div class="woocommerce-error">' . esc_html__( 'Invalid Subscription.', 'woocommerce-subscriptions' ) . ' <a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '" class="wc-forward">'. esc_html__( 'My Account', 'woocommerce-subscriptions' ) .'</a>' . '</div>';
return;
}
wc_get_template( 'myaccount/view-subscription.php', compact( 'subscription' ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
} }
/** /**
@@ -62,4 +50,16 @@ class WCS_Template_Loader {
public static function get_subscription_totals_template( $subscription ) { public static function get_subscription_totals_template( $subscription ) {
wc_get_template( 'myaccount/subscription-totals.php', array( 'subscription' => $subscription ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' ); wc_get_template( 'myaccount/subscription-totals.php', array( 'subscription' => $subscription ), '', plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'templates/' );
} }
/**
* Get the order downloads template, which is part of the view subscription page.
*
* @param WC_Subscription $subscription Subscription object
* @since 2.5.0
*/
public static function get_order_downloads_template( $subscription ) {
if ( $subscription->has_downloadable_item() && $subscription->is_download_permitted() ) {
wc_get_template( 'order/order-downloads.php', array( 'downloads' => $subscription->get_downloadable_items(), 'show_title' => true ) );
}
}
} }

View File

@@ -155,20 +155,18 @@ 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. * 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. * 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. * To make sure all subscriptions are linked to the correct customer, we revert to the previous behavior of
* This will be eventually removed. * getting the customer user from post meta.
* The fix is only applied on WC 3.5.0 because 3.5.1 brought back the old way (pre 3.5.0) of getting the
* customer ID for orders.
* *
* @see https://github.com/Prospress/woocommerce-subscriptions/issues/3036 * @see https://github.com/Prospress/woocommerce-subscriptions/issues/3036
*/ */
if ( '3.5.0' === WC()->version ) {
$props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true ); $props_to_set['customer_id'] = get_post_meta( $subscription->get_id(), '_customer_user', true );
} }

View File

@@ -25,6 +25,7 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
// Check if a user is requesting to create an early renewal order for a subscription. // Check if a user is requesting to create an early renewal order for a subscription.
add_action( 'template_redirect', array( $this, 'maybe_setup_cart' ), 100 ); add_action( 'template_redirect', array( $this, 'maybe_setup_cart' ), 100 );
add_action( 'woocommerce_checkout_create_order', array( $this, 'copy_subscription_meta_to_order' ), 90 );
// Record early renewal payments. // Record early renewal payments.
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) { if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
add_action( 'woocommerce_checkout_order_processed', array( $this, 'maybe_record_early_renewal' ), 100, 2 ); add_action( 'woocommerce_checkout_order_processed', array( $this, 'maybe_record_early_renewal' ), 100, 2 );
@@ -149,6 +150,26 @@ class WCS_Cart_Early_Renewal extends WCS_Cart_Renewal {
$subscription->update_status( 'on-hold', _x( 'Customer requested to renew early:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) ); $subscription->update_status( 'on-hold', _x( 'Customer requested to renew early:', 'used in order note as reason for why subscription status changed', 'woocommerce-subscriptions' ) );
} }
/**
* Copies the metadata from the subscription to the order created on checkout.
*
* @param WC_Order $order The WC Order object.
*
* @since 2.5.2
*/
public function copy_subscription_meta_to_order( $order ) {
$cart_item = $this->cart_contains();
if ( ! $cart_item ) {
return;
}
// Get the subscription.
$subscription = wcs_get_subscription( $cart_item[ $this->cart_item_key ]['subscription_id'] );
// Copy all meta from subscription to new renewal order
wcs_copy_order_meta( $subscription, $order, 'renewal_order' );
}
/** /**
* Adds the early renewal metadata to the order created on checkout. * Adds the early renewal metadata to the order created on checkout.
* *

View File

@@ -45,6 +45,26 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
} }
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *
@@ -131,22 +151,22 @@ class WCS_Email_Cancelled_Subscription extends WC_Email {
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ), 'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
// translators: placeholder is admin email // translators: placeholder is admin email
'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to <code>%s</code>.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ), 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ),
'placeholder' => '', 'placeholder' => '',
'default' => '', 'default' => '',
), ),
'subject' => array( 'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ), 'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->subject ), 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '<code>' . $this->subject . '</code>' ),
'placeholder' => '', 'placeholder' => $this->get_default_subject(),
'default' => '', 'default' => '',
), ),
'heading' => array( 'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ), 'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ), 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: %s.', 'woocommerce-subscriptions' ), '<code>' . $this->heading . '</code>' ),
'placeholder' => '', 'placeholder' => $this->get_default_heading(),
'default' => '', 'default' => '',
), ),
'email_type' => array( 'email_type' => array(

View File

@@ -46,6 +46,26 @@ class WCS_Email_Completed_Renewal_Order extends WC_Email_Customer_Completed_Orde
WC_Email::__construct(); WC_Email::__construct();
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *

View File

@@ -45,6 +45,26 @@ class WCS_Email_Completed_Switch_Order extends WC_Email_Customer_Completed_Order
WC_Email::__construct(); WC_Email::__construct();
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *

View File

@@ -36,6 +36,28 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
WC_Email::__construct(); WC_Email::__construct();
} }
/**
* Get the default e-mail subject.
*
* @param bool $paid Whether the order has been paid or not.
* @since 2.5.3
* @return string
*/
public function get_default_subject( $paid = false ) {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @param bool $paid Whether the order has been paid or not.
* @since 2.5.3
* @return string
*/
public function get_default_heading( $paid = false ) {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *
@@ -53,9 +75,9 @@ class WCS_Email_Customer_Payment_Retry extends WCS_Email_Customer_Renewal_Invoic
$retry_time_index = array_search( '{retry_time}', $this->find ); $retry_time_index = array_search( '{retry_time}', $this->find );
if ( false === $retry_time_index ) { if ( false === $retry_time_index ) {
$this->find['retry_time'] = '{retry_time}'; $this->find['retry_time'] = '{retry_time}';
$this->replace['retry_time'] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) ); $this->replace['retry_time'] = wcs_get_human_time_diff( $this->retry->get_time() );
} else { } else {
$this->replace[ $retry_time_index ] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) ); $this->replace[ $retry_time_index ] = wcs_get_human_time_diff( $this->retry->get_time() );
} }
parent::trigger( $order_id, $order ); parent::trigger( $order_id, $order );

View File

@@ -56,6 +56,28 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
WC_Email::__construct(); WC_Email::__construct();
} }
/**
* Get the default e-mail subject.
*
* @param bool $paid Whether the order has been paid or not.
* @since 2.5.3
* @return string
*/
public function get_default_subject( $paid = false ) {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @param bool $paid Whether the order has been paid or not.
* @since 2.5.3
* @return string
*/
public function get_default_heading( $paid = false ) {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *

View File

@@ -45,6 +45,26 @@ class WCS_Email_Expired_Subscription extends WC_Email {
} }
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *
@@ -129,22 +149,22 @@ class WCS_Email_Expired_Subscription extends WC_Email {
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ), 'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
// translators: placeholder is admin email // translators: placeholder is admin email
'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to <code>%s</code>.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ), 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '<code>' . esc_html( get_option( 'admin_email' ) ) . '</code>' ),
'placeholder' => '', 'placeholder' => '',
'default' => '', 'default' => '',
), ),
'subject' => array( 'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ), 'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->subject ), 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '<code>' . $this->subject . '</code>' ),
'placeholder' => '', 'placeholder' => $this->get_default_subject(),
'default' => '', 'default' => '',
), ),
'heading' => array( 'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ), 'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ), 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ),
'placeholder' => '', 'placeholder' => $this->get_default_heading(),
'default' => '', 'default' => '',
), ),
'email_type' => array( 'email_type' => array(

View File

@@ -48,6 +48,26 @@ class WCS_Email_New_Renewal_Order extends WC_Email_New_Order {
} }
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *

View File

@@ -43,6 +43,26 @@ class WCS_Email_New_Switch_Order extends WC_Email_New_Order {
} }
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *

View File

@@ -45,6 +45,26 @@ class WCS_Email_On_Hold_Subscription extends WC_Email {
} }
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *
@@ -129,22 +149,22 @@ class WCS_Email_On_Hold_Subscription extends WC_Email {
'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ), 'title' => _x( 'Recipient(s)', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
// translators: placeholder is admin email // translators: placeholder is admin email
'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to <code>%s</code>.', 'woocommerce-subscriptions' ), esc_attr( get_option( 'admin_email' ) ) ), 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce-subscriptions' ), '<code>' . esc_attr( get_option( 'admin_email' ) ) . '</code>' ),
'placeholder' => '', 'placeholder' => '',
'default' => '', 'default' => '',
), ),
'subject' => array( 'subject' => array(
'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ), 'title' => _x( 'Subject', 'of an email', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->subject ), 'description' => sprintf( __( 'This controls the email subject line. Leave blank to use the default subject: %s.', 'woocommerce-subscriptions' ), '<code>' . $this->subject . '</code>' ),
'placeholder' => '', 'placeholder' => $this->get_default_subject(),
'default' => '', 'default' => '',
), ),
'heading' => array( 'heading' => array(
'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ), 'title' => _x( 'Email Heading', 'Name the setting that controls the main heading contained within the email notification', 'woocommerce-subscriptions' ),
'type' => 'text', 'type' => 'text',
'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ), 'description' => sprintf( __( 'This controls the main heading contained within the email notification. Leave blank to use the default heading: <code>%s</code>.', 'woocommerce-subscriptions' ), $this->heading ),
'placeholder' => '', 'placeholder' => $this->get_default_heading(),
'default' => '', 'default' => '',
), ),
'email_type' => array( 'email_type' => array(

View File

@@ -40,6 +40,26 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
$this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) );
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* Trigger. * Trigger.
* *
@@ -53,7 +73,7 @@ class WCS_Email_Payment_Retry extends WC_Email_Failed_Order {
$this->find['retry-time'] = '{retry_time}'; $this->find['retry-time'] = '{retry_time}';
$this->replace['order-date'] = wcs_format_datetime( wcs_get_objects_property( $this->object, 'date_created' ) ); $this->replace['order-date'] = wcs_format_datetime( wcs_get_objects_property( $this->object, 'date_created' ) );
$this->replace['order-number'] = $this->object->get_order_number(); $this->replace['order-number'] = $this->object->get_order_number();
$this->replace['retry-time'] = strtolower( wcs_get_human_time_diff( $this->retry->get_time() ) ); $this->replace['retry-time'] = wcs_get_human_time_diff( $this->retry->get_time() );
if ( ! $this->is_enabled() || ! $this->get_recipient() ) { if ( ! $this->is_enabled() || ! $this->get_recipient() ) {
return; return;

View File

@@ -40,6 +40,26 @@ class WCS_Email_Processing_Renewal_Order extends WC_Email_Customer_Processing_Or
WC_Email::__construct(); WC_Email::__construct();
} }
/**
* Get the default e-mail subject.
*
* @since 2.5.3
* @return string
*/
public function get_default_subject() {
return $this->subject;
}
/**
* Get the default e-mail heading.
*
* @since 2.5.3
* @return string
*/
public function get_default_heading() {
return $this->heading;
}
/** /**
* trigger function. * trigger function.
* *

View File

@@ -21,16 +21,18 @@ class WC_Subscriptions_Payment_Gateways {
*/ */
public static function init() { public static function init() {
add_action( 'init', __CLASS__ . '::init_paypal', 5 ); // run before default priority 10 in case the site is using ALTERNATE_WP_CRON to avoid https://core.trac.wordpress.org/ticket/24160 add_action( 'init', __CLASS__ . '::init_paypal', 5 ); // run before default priority 10 in case the site is using ALTERNATE_WP_CRON to avoid https://core.trac.wordpress.org/ticket/24160.
add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways' ); add_filter( 'woocommerce_available_payment_gateways', __CLASS__ . '::get_available_payment_gateways' );
add_filter( 'woocommerce_no_available_payment_methods_message', __CLASS__ . '::no_available_payment_methods_message' ); add_filter( 'woocommerce_no_available_payment_methods_message', __CLASS__ . '::no_available_payment_methods_message' );
// Trigger a hook for gateways to charge recurring payments add_filter( 'woocommerce_payment_gateways_renewal_support_status_html', __CLASS__ . '::payment_gateways_support_tooltip', 11, 2 );
// Trigger a hook for gateways to charge recurring payments.
add_action( 'woocommerce_scheduled_subscription_payment', __CLASS__ . '::gateway_scheduled_subscription_payment', 10, 1 ); add_action( 'woocommerce_scheduled_subscription_payment', __CLASS__ . '::gateway_scheduled_subscription_payment', 10, 1 );
// Create a gateway specific hooks for subscription events // Create a gateway specific hooks for subscription events.
add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::trigger_gateway_status_updated_hook', 10, 2 ); add_action( 'woocommerce_subscription_status_updated', __CLASS__ . '::trigger_gateway_status_updated_hook', 10, 2 );
} }
@@ -214,6 +216,73 @@ class WC_Subscriptions_Payment_Gateways {
} }
} }
/**
* Display a list of each gateway supported features in a tooltip
*
* @since 2.5.0
*/
public static function payment_gateways_support_tooltip( $status_html, $gateway ) {
if ( ( ! is_array( $gateway->supports ) || ! in_array( 'subscriptions', $gateway->supports ) ) && 'paypal' !== $gateway->id ) {
return $status_html;
}
$core_features = $gateway->supports;
$subscription_features = $change_payment_method_features = array();
foreach ( $core_features as $key => $feature ) {
// Skip any non-subscription related features.
if ( 0 !== strpos( $feature, 'subscription' ) ) {
continue;
}
$feature = str_replace( 'subscription_', '', $feature );
if ( 0 === strpos( $feature, 'payment_method' ) ) {
switch ( $feature ) {
case 'payment_method_change':
$change_payment_method_features[] = 'payment method change';
break;
case 'payment_method_change_customer':
$change_payment_method_features[] = 'customer change payment';
break;
case 'payment_method_change_admin':
$change_payment_method_features[] = 'admin change payment';
break;
default:
$change_payment_method_features[] = str_replace( 'payment_method', ' ', $feature );
break;
}
} else {
$subscription_features[] = $feature;
}
unset( $core_features[ $key ] );
}
$status_html .= '<span class="payment-method-features-info tips" data-tip="';
$status_html .= esc_attr( '<strong><u>' . __( 'Supported features:', 'woocommerce-subscriptions' ) . '</u></strong></br>' . implode( '<br />', str_replace( '_', ' ', $core_features ) ) );
if ( ! empty( $subscription_features ) ) {
$status_html .= esc_attr( '</br><strong><u>' . __( 'Subscription features:', 'woocommerce-subscriptions' ) . '</u></strong></br>' . implode( '<br />', str_replace( '_', ' ', $subscription_features ) ) );
}
if ( ! empty( $change_payment_method_features ) ) {
$status_html .= esc_attr( '</br><strong><u>' . __( 'Change payment features:', 'woocommerce-subscriptions' ) . '</u></strong></br>' . implode( '<br />', str_replace( '_', ' ', $change_payment_method_features ) ) );
}
$status_html .= '"></span>';
$allowed_html = wp_kses_allowed_html( 'post' );
$allowed_html['span']['data-tip'] = true;
return wp_kses( $status_html, $allowed_html );
}
/** /**
* Fire a gateway specific hook for when a subscription is activated. * Fire a gateway specific hook for when a subscription is activated.
* *

View File

@@ -34,7 +34,7 @@ class WCS_PayPal {
* Main PayPal Instance, ensures only one instance is/can be loaded * Main PayPal Instance, ensures only one instance is/can be loaded
* *
* @see wc_paypal_express() * @see wc_paypal_express()
* @return WC_PayPal_Express * @return WCS_PayPal
* @since 2.0 * @since 2.0
*/ */
public static function instance() { public static function instance() {
@@ -80,18 +80,32 @@ class WCS_PayPal {
add_filter( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', __CLASS__ . '::maybe_add_change_payment_method_warning' ); add_filter( 'woocommerce_subscriptions_admin_meta_boxes_script_parameters', __CLASS__ . '::maybe_add_change_payment_method_warning' );
// Maybe order don't need payment because lock.
add_filter( 'woocommerce_order_needs_payment', __CLASS__ . '::maybe_override_needs_payment', 10, 2 );
// Remove payment lock when order is completely paid or order is cancelled.
add_action( 'woocommerce_order_status_cancelled', __CLASS__ . '::maybe_remove_payment_lock' );
add_action( 'woocommerce_payment_complete', __CLASS__ . '::maybe_remove_payment_lock' );
// Adds payment lock on order received.
add_action( 'get_header', __CLASS__ . '::maybe_add_payment_lock' );
// Run the IPN failure handler attach and detach functions before and after processing to catch and log any unexpected shutdowns // Run the IPN failure handler attach and detach functions before and after processing to catch and log any unexpected shutdowns
add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::attach', -1, 1 ); add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::attach', -1, 1 );
add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::detach', 1, 1 ); add_action( 'valid-paypal-standard-ipn-request', 'WCS_PayPal_Standard_IPN_Failure_Handler::detach', 1, 1 );
// Remove PayPal from the available payment methods if it's disabled for subscription purchases.
add_filter( 'woocommerce_available_payment_gateways', array( __CLASS__, 'maybe_remove_paypal_standard' ) );
WCS_PayPal_Supports::init(); WCS_PayPal_Supports::init();
WCS_PayPal_Status_Manager::init(); WCS_PayPal_Status_Manager::init();
WCS_PayPal_Standard_Switcher::init(); WCS_PayPal_Standard_Switcher::init();
if ( is_admin() ) { if ( is_admin() ) {
WCS_PayPal_Admin::init(); WCS_PayPal_Admin::init();
WCS_PayPal_Change_Payment_Method_Admin::init();
} }
WCS_PayPal_Change_Payment_Method_Admin::init();
} }
/** /**
@@ -227,6 +241,11 @@ class WCS_PayPal {
// Store the billing agreement ID on the order and subscriptions // Store the billing agreement ID on the order and subscriptions
wcs_set_paypal_id( $order, $billing_agreement_response->get_billing_agreement_id() ); wcs_set_paypal_id( $order, $billing_agreement_response->get_billing_agreement_id() );
// Update payment method on all active subscriptions?
if ( wcs_is_subscription( $order ) && WC_Subscriptions_Change_Payment_Gateway::will_subscription_update_all_payment_methods( $order ) ) {
WC_Subscriptions_Change_Payment_Gateway::update_all_payment_methods_from_subscription( $order, $payment_method->id );
}
foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) ) as $subscription ) { foreach ( wcs_get_subscriptions_for_order( $order, array( 'order_type' => 'any' ) ) as $subscription ) {
$subscription->set_payment_method( $payment_method ); $subscription->set_payment_method( $payment_method );
wcs_set_paypal_id( $subscription, $billing_agreement_response->get_billing_agreement_id() ); // Also saves the subscription wcs_set_paypal_id( $subscription, $billing_agreement_response->get_billing_agreement_id() ); // Also saves the subscription
@@ -438,6 +457,74 @@ class WCS_PayPal {
return $script_parameters; return $script_parameters;
} }
/**
* This validates against payment lock for PP and returns false if we meet the criteria:
* - is a parent order.
* - payment method is paypal.
* - PayPal Reference Transactions is disabled.
* - order has lock.
* - lock hasn't timeout.
*
* @param bool $needs_payment Does this order needs to process payment?
* @param WC_Order $order The actual order.
*
* @return bool
* @since 2.5.3
*/
public static function maybe_override_needs_payment( $needs_payment, $order ) {
if ( $needs_payment && self::instance()->get_id() === $order->get_payment_method() && ! self::are_reference_transactions_enabled() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) {
$has_lock = $order->get_meta( '_wcs_lock_order_payment' );
$seconds_since_order = wcs_seconds_since_order_created( $order );
// We have lock and order hasn't meet the lock time.
if ( $has_lock && $seconds_since_order < apply_filters( 'wcs_lock_order_payment_seconds', 180 ) ) {
$needs_payment = false;
}
}
return $needs_payment;
}
/**
* Adds payment lock meta when order is received and...
* - order is valid.
* - payment method is paypal.
* - order needs payment.
* - PayPal Reference Transactions is disabled.
* - order is parent order of a subscription.
*
* @since 2.5.3
*/
public static function maybe_add_payment_lock() {
if ( ! wcs_is_order_received_page() ) {
return;
}
global $wp;
$order = wc_get_order( absint( $wp->query_vars['order-received'] ) );
if ( $order && self::instance()->get_id() === $order->get_payment_method() && $order->needs_payment() && ! self::are_reference_transactions_enabled() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) {
$order->update_meta_data( '_wcs_lock_order_payment', 'true' );
$order->save();
}
}
/**
* Removes payment lock when order is parent and has paypal method.
*
* @param int $order_id Order cancelled/paid.
*
* @since 2.5.3
*/
public static function maybe_remove_payment_lock( $order_id ) {
$order = wc_get_order( $order_id );
if ( self::instance()->get_id() === $order->get_payment_method() && wcs_order_contains_subscription( $order, array( 'parent' ) ) ) {
$order->delete_meta_data( 'wcs_lock_order_payment' );
$order->save();
}
}
/** Getters ******************************************************/ /** Getters ******************************************************/
/** /**
@@ -540,4 +627,65 @@ class WCS_PayPal {
return 'paypal'; return 'paypal';
} }
/**
* Set the default value for whether PayPal Standard is enabled or disabled for subscriptions purchases.
*
* PayPal Standard will be enabled for subscriptions when:
* - PayPal is enabled.
* - The store has existing subscriptions.
*
* In any other case, it will be disabled by default.
* This function is called when 2.5.0 is active for the first time. @see WC_Subscriptions_Upgrader::upgrade()
*
* @since 2.5.0
*/
public static function set_enabled_for_subscriptions_default() {
// Exit early if it has already been set.
if ( self::get_option( 'enabled_for_subscriptions' ) ) {
return;
}
// For existing stores with PayPal enabled, PayPal is automatically enabled for subscriptions.
if ( 'yes' === WCS_PayPal::get_option( 'enabled' ) && wcs_do_subscriptions_exist() ) {
$default = 'yes';
} else {
$default = 'no';
}
// Find the PayPal Standard gateway instance to set the setting.
foreach ( WC()->payment_gateways->payment_gateways as $gateway ) {
if ( $gateway->id === 'paypal' ) {
wcs_update_settings_option( $gateway, 'enabled_for_subscriptions', $default );
break;
}
}
}
/**
* Remove PayPal Standard as an available payment method if it is disabled for subscriptions.
*
* @param array $available_gateways A list of available payment methods displayed on the checkout.
* @return array
* @since 2.5.0
*/
public static function maybe_remove_paypal_standard( $available_gateways ) {
if ( ! isset( $available_gateways['paypal'] ) || 'yes' === self::get_option( 'enabled_for_subscriptions' ) || WCS_PayPal::are_reference_transactions_enabled() ) {
return $available_gateways;
}
$paying_for_order = absint( get_query_var( 'order-pay' ) );
if (
WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment ||
WC_Subscriptions_Cart::cart_contains_subscription() ||
wcs_cart_contains_renewal() ||
( $paying_for_order && wcs_order_contains_subscription( $paying_for_order ) )
) {
unset( $available_gateways['paypal'] );
}
return $available_gateways;
}
} }

View File

@@ -39,6 +39,9 @@ class WCS_PayPal_Admin {
// Before WC updates the PayPal settings remove credentials error flag // Before WC updates the PayPal settings remove credentials error flag
add_action( 'load-woocommerce_page_wc-settings', __CLASS__ . '::maybe_update_credentials_error_flag', 9 ); add_action( 'load-woocommerce_page_wc-settings', __CLASS__ . '::maybe_update_credentials_error_flag', 9 );
// Add an enable for subscription purchases setting.
add_action( 'woocommerce_settings_api_form_fields_paypal', array( __CLASS__, 'add_enable_for_subscriptions_setting' ) );
} }
/** /**
@@ -56,7 +59,8 @@ class WCS_PayPal_Admin {
// Warn store managers not to change their PayPal Email address as it can break existing Subscriptions in WC2.0+ // Warn store managers not to change their PayPal Email address as it can break existing Subscriptions in WC2.0+
WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['desc_tip'] = false; WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['desc_tip'] = false;
WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['description'] .= ' </p><p class="description">' . __( 'It is <strong>strongly recommended you do not change the Receiver Email address</strong> if you have active subscriptions with PayPal. Doing so can break existing subscriptions.', 'woocommerce-subscriptions' ); // translators: $1 and $2 are opening and closing strong tags, respectively.
WC()->payment_gateways->payment_gateways[ $key ]->form_fields['receiver_email']['description'] .= ' </p><p class="description">' . sprintf( __( 'It is %sstrongly recommended you do not change the Receiver Email address%s if you have active subscriptions with PayPal. Doing so can break existing subscriptions.', 'woocommerce-subscriptions' ), '<strong>', '</strong>' );
} }
} }
@@ -244,7 +248,7 @@ class WCS_PayPal_Admin {
* @param WC_Subscription $subscription * @param WC_Subscription $subscription
*/ */
public static function profile_link( $subscription ) { public static function profile_link( $subscription ) {
if ( wcs_is_subscription( $subscription ) && 'paypal' == $subscription->get_payment_method() ) { if ( wcs_is_subscription( $subscription ) && ! $subscription->is_manual() && 'paypal' == $subscription->get_payment_method() ) {
$paypal_profile_id = wcs_get_paypal_id( $subscription ); $paypal_profile_id = wcs_get_paypal_id( $subscription );
@@ -275,4 +279,35 @@ class WCS_PayPal_Admin {
} }
/**
* Add the enabled or subscriptions setting.
*
* @param array $settings The WooCommerce PayPal Settings array.
* @return array
* @since 2.5.0
*/
public static function add_enable_for_subscriptions_setting( $settings ) {
if ( WCS_PayPal::are_reference_transactions_enabled() ) {
return $settings;
}
$setting = array(
'type' => 'checkbox',
'label' => __( 'Enable PayPal Standard for Subscriptions', 'woocommerce-subscriptions' ),
'default' => 'no',
);
// Display a description
if ( 'no' === WCS_PayPal::get_option( 'enabled_for_subscriptions' ) ) {
$setting['description'] = sprintf(
/* translators: Placeholders are the opening and closing link tags.*/
__( "Before enabling PayPal Standard for Subscriptions, please note, when using PayPal Standard, customers are locked into using PayPal Standard for the life of their subscription, and PayPal Standard has a number of limitations. Please read the guide on %swhy we don't recommend PayPal Standard%s for Subscriptions before choosing to enable this option.", 'woocommerce-subscriptions' ),
'<a href="https://docs.woocommerce.com/document/subscriptions/payment-gateways/#paypal-limitations">', '</a>'
);
}
$settings = wcs_array_insert_after( 'enabled', $settings, 'enabled_for_subscriptions', $setting );
return $settings;
}
} }

View File

@@ -28,8 +28,10 @@ class WCS_PayPal_Change_Payment_Method_Admin {
add_filter( 'woocommerce_subscription_payment_meta', __CLASS__ . '::add_payment_meta_details', 10, 2 ); add_filter( 'woocommerce_subscription_payment_meta', __CLASS__ . '::add_payment_meta_details', 10, 2 );
// Validate the PayPal billing agreement ID meta value when attempting to set PayPal as the payment method // Validate the PayPal billing agreement ID meta value when attempting to set PayPal as the payment method
if ( is_admin() ) {
add_filter( 'woocommerce_subscription_validate_payment_meta_paypal', __CLASS__ . '::validate_payment_meta', 10, 2 ); add_filter( 'woocommerce_subscription_validate_payment_meta_paypal', __CLASS__ . '::validate_payment_meta', 10, 2 );
} }
}
/** /**
* Include the PayPal payment meta data required to process automatic recurring payments so that store managers can * Include the PayPal payment meta data required to process automatic recurring payments so that store managers can

View File

@@ -252,7 +252,7 @@ class WCS_PayPal_Reference_Transaction_API_Request {
'ITEMURL' => $product->get_permalink(), 'ITEMURL' => $product->get_permalink(),
); );
$order_subtotal += $item['line_total']; $order_subtotal += $order->get_line_total( $item );
} }
// add fees // add fees
@@ -264,7 +264,7 @@ class WCS_PayPal_Reference_Transaction_API_Request {
'QTY' => 1, 'QTY' => 1,
); );
$order_subtotal += $fee['line_total']; $order_subtotal += $order->get_line_total( $fee );
} }
// add discounts // add discounts
@@ -550,12 +550,11 @@ class WCS_PayPal_Reference_Transaction_API_Request {
/** /**
* Returns the request parameters after validation & filtering * Returns the request parameters after validation & filtering
* *
* @throws \SV_WC_Payment_Gateway_Exception invalid amount * @throws \Exception invalid amount
* @return array request parameters * @return array request parameters
* @since 2.0 * @since 2.0
*/ */
public function get_parameters() { public function get_parameters() {
/** /**
* Filter PPE request parameters. * Filter PPE request parameters.
* *
@@ -579,8 +578,7 @@ class WCS_PayPal_Reference_Transaction_API_Request {
// amounts must be 10,000.00 or less for USD // amounts must be 10,000.00 or less for USD
if ( isset( $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] ) && 'USD' == $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] && $value > 10000 ) { if ( isset( $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] ) && 'USD' == $this->parameters['PAYMENTREQUEST_0_CURRENCYCODE'] && $value > 10000 ) {
throw new Exception( sprintf( '%s amount of %s must be less than $10,000.00', $key, wc_price( $value ) ) );
throw new SV_WC_Payment_Gateway_Exception( sprintf( '%s amount of %s must be less than $10,000.00', $key, $value ) );
} }
// PayPal requires locale-specific number formats (e.g. USD is 123.45) // PayPal requires locale-specific number formats (e.g. USD is 123.45)

View File

@@ -248,15 +248,16 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
switch ( $transaction_details['txn_type'] ) { switch ( $transaction_details['txn_type'] ) {
case 'subscr_signup': case 'subscr_signup':
$order = self::get_parent_order_with_fallback( $subscription );
// Store PayPal Details on Subscription and Order // Store PayPal Details on Subscription and Order
$this->save_paypal_meta_data( $subscription, $transaction_details ); $this->save_paypal_meta_data( $subscription, $transaction_details );
$this->save_paypal_meta_data( $subscription->get_parent(), $transaction_details ); $this->save_paypal_meta_data( $order, $transaction_details );
// When there is a free trial & no initial payment amount, we need to mark the order as paid and activate the subscription // When there is a free trial & no initial payment amount, we need to mark the order as paid and activate the subscription
if ( ! $is_payment_change && ! $is_renewal_sign_up_after_failure && 0 == $subscription->get_parent()->get_total() ) { if ( ! $is_payment_change && ! $is_renewal_sign_up_after_failure && 0 == $order->get_total() ) {
// Safe to assume the subscription has an order here because otherwise we wouldn't get a 'subscr_signup' IPN // Safe to assume the subscription has an order here because otherwise we wouldn't get a 'subscr_signup' IPN
$subscription->get_parent()->payment_complete(); // No 'txn_id' value for 'subscr_signup' IPN messages $order->payment_complete(); // No 'txn_id' value for 'subscr_signup' IPN messages
update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' ); update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' );
} }
@@ -288,8 +289,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
break; break;
case 'subscr_payment': case 'subscr_payment':
if ( 0.01 == $transaction_details['mc_gross'] ) {
if ( 0.01 == $transaction_details['mc_gross'] && 1 == $subscription->get_completed_payment_count() ) {
WC_Gateway_Paypal::log( 'IPN ignored, treating IPN as secondary trial period.' ); WC_Gateway_Paypal::log( 'IPN ignored, treating IPN as secondary trial period.' );
exit; exit;
} }
@@ -335,17 +335,18 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
// First payment on order, process payment & activate subscription // First payment on order, process payment & activate subscription
if ( $is_first_payment ) { if ( $is_first_payment ) {
$parent_order = $subscription->get_parent(); $parent_order = self::get_parent_order_with_fallback( $subscription );
if ( ! $parent_order->is_paid() ) { if ( ! $parent_order->is_paid() ) {
$parent_order->payment_complete( $transaction_details['txn_id'] ); $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.
if ( $subscription->can_be_updated_to( 'active' ) ) {
$subscription->update_status( 'active' ); $subscription->update_status( 'active' );
} }
// Store PayPal Details on Order // Store PayPal Details on Order
$this->save_paypal_meta_data( $subscription->get_parent(), $transaction_details ); $this->save_paypal_meta_data( $parent_order, $transaction_details );
// IPN got here first or PDT will never arrive. Normally PDT would have arrived, so the first IPN would not be the first payment. In case the the first payment is an IPN, we need to make sure to not ignore the second one // IPN got here first or PDT will never arrive. Normally PDT would have arrived, so the first IPN would not be the first payment. In case the the first payment is an IPN, we need to make sure to not ignore the second one
update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' ); update_post_meta( $subscription->get_id(), '_paypal_first_ipn_ignored_for_pdt', 'true' );
@@ -457,7 +458,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
// Make sure subscription hasn't been linked to a new payment method // Make sure subscription hasn't been linked to a new payment method
if ( wcs_get_paypal_id( $subscription ) != $ipn_profile_id ) { if ( wcs_get_paypal_id( $subscription ) != $ipn_profile_id ) {
WC_Gateway_Paypal::log( sprintf( 'IPN "recurring_payment_suspended" ignored for subscription %d - PayPal profile ID has changed', $subscription->id ) ); WC_Gateway_Paypal::log( sprintf( 'IPN "recurring_payment_suspended" ignored for subscription %d - PayPal profile ID has changed', $subscription->get_id() ) );
} else if ( $subscription->has_status( 'active' ) ) { } else if ( $subscription->has_status( 'active' ) ) {
@@ -648,6 +649,22 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
return array( 'order_id' => (int) $order_id, 'order_key' => $order_key ); return array( 'order_id' => (int) $order_id, 'order_key' => $order_key );
} }
/**
* This function will try to get the parent order, and if not available, will get the last order related to the Subscription.
*
* @param WC_Subscription $subscription The Subscription.
*
* @return WC_Order Parent order or the last related order (renewal)
*/
protected static function get_parent_order_with_fallback( $subscription ) {
$order = $subscription->get_parent();
if ( ! $order ) {
$order = $subscription->get_last_order( 'all' );
}
return $order;
}
/** /**
* Cancel a specific PayPal Standard Subscription Profile with PayPal. * Cancel a specific PayPal Standard Subscription Profile with PayPal.
* *

View File

@@ -35,6 +35,7 @@ class WCS_PayPal_Supports {
'subscription_amount_changes', 'subscription_amount_changes',
'subscription_date_changes', 'subscription_date_changes',
'multiple_subscriptions', 'multiple_subscriptions',
'subscription_payment_method_delayed_change',
); );
/** /**

View File

@@ -5,7 +5,7 @@
* Description: A robust scheduling library for use in WordPress plugins. * Description: A robust scheduling library for use in WordPress plugins.
* Author: Prospress * Author: Prospress
* Author URI: http://prospress.com/ * Author URI: http://prospress.com/
* Version: 2.1.1 * Version: 2.2.1
* License: GPLv3 * License: GPLv3
* *
* Copyright 2018 Prospress, Inc. (email : freedoms@prospress.com) * Copyright 2018 Prospress, Inc. (email : freedoms@prospress.com)
@@ -25,21 +25,21 @@
* *
*/ */
if ( ! function_exists( 'action_scheduler_register_2_dot_1_dot_1' ) ) { if ( ! function_exists( 'action_scheduler_register_2_dot_2_dot_1' ) ) {
if ( ! class_exists( 'ActionScheduler_Versions' ) ) { if ( ! class_exists( 'ActionScheduler_Versions' ) ) {
require_once( 'classes/ActionScheduler_Versions.php' ); require_once( 'classes/ActionScheduler_Versions.php' );
add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 );
} }
add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_1_dot_1', 0, 0 ); add_action( 'plugins_loaded', 'action_scheduler_register_2_dot_2_dot_1', 0, 0 );
function action_scheduler_register_2_dot_1_dot_1() { function action_scheduler_register_2_dot_2_dot_1() {
$versions = ActionScheduler_Versions::instance(); $versions = ActionScheduler_Versions::instance();
$versions->register( '2.1.1', 'action_scheduler_initialize_2_dot_1_dot_1' ); $versions->register( '2.2.1', 'action_scheduler_initialize_2_dot_2_dot_1' );
} }
function action_scheduler_initialize_2_dot_1_dot_1() { function action_scheduler_initialize_2_dot_2_dot_1() {
require_once( 'classes/ActionScheduler.php' ); require_once( 'classes/ActionScheduler.php' );
ActionScheduler::init( __FILE__ ); ActionScheduler::init( __FILE__ );
} }

View File

@@ -85,6 +85,11 @@ abstract class ActionScheduler {
self::$plugin_file = $plugin_file; self::$plugin_file = $plugin_file;
spl_autoload_register( array( __CLASS__, 'autoload' ) ); spl_autoload_register( array( __CLASS__, 'autoload' ) );
/**
* Fires in the early stages of Action Scheduler init hook.
*/
do_action( 'action_scheduler_pre_init' );
$store = self::store(); $store = self::store();
add_action( 'init', array( $store, 'init' ), 1, 0 ); add_action( 'init', array( $store, 'init' ), 1, 0 );

View File

@@ -57,14 +57,17 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
$action = $this->store->fetch_action( $action_id ); $action = $this->store->fetch_action( $action_id );
$this->store->log_execution( $action_id ); $this->store->log_execution( $action_id );
$action->execute(); $action->execute();
do_action( 'action_scheduler_after_execute', $action_id ); do_action( 'action_scheduler_after_execute', $action_id, $action );
$this->store->mark_complete( $action_id ); $this->store->mark_complete( $action_id );
} catch ( Exception $e ) { } catch ( Exception $e ) {
$this->store->mark_failure( $action_id ); $this->store->mark_failure( $action_id );
do_action( 'action_scheduler_failed_execution', $action_id, $e ); do_action( 'action_scheduler_failed_execution', $action_id, $e );
} }
if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) ) {
$this->schedule_next_instance( $action ); $this->schedule_next_instance( $action );
} }
}
/** /**
* Schedule the next instance of the action if necessary. * Schedule the next instance of the action if necessary.
@@ -86,7 +89,7 @@ abstract class ActionScheduler_Abstract_QueueRunner extends ActionScheduler_Abst
* @author Jeremy Pry * @author Jeremy Pry
*/ */
protected function run_cleanup() { protected function run_cleanup() {
$this->cleaner->clean(); $this->cleaner->clean( 10 * $this->get_time_limit() );
} }
/** /**

View File

@@ -30,6 +30,7 @@ class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated {
if ( class_exists( 'WooCommerce' ) ) { if ( class_exists( 'WooCommerce' ) ) {
add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) ); add_action( 'woocommerce_admin_status_content_action-scheduler', array( $this, 'render_admin_ui' ) );
add_action( 'woocommerce_system_status_report', array( $this, 'system_status_report' ) );
add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) ); add_filter( 'woocommerce_admin_status_tabs', array( $this, 'register_system_status_tab' ) );
} }
@@ -37,6 +38,10 @@ class ActionScheduler_AdminView extends ActionScheduler_AdminView_Deprecated {
} }
} }
public function system_status_report() {
$table = new ActionScheduler_wcSystemStatus( ActionScheduler::store() );
$table->print();
}
/** /**
* Registers action-scheduler into WooCommerce > System status. * Registers action-scheduler into WooCommerce > System status.

View File

@@ -31,6 +31,13 @@ class ActionScheduler_CronSchedule implements ActionScheduler_Schedule {
return true; return true;
} }
/**
* @return string
*/
public function get_recurrence() {
return strval($this->cron);
}
/** /**
* For PHP 5.2 compat, since DateTime objects can't be serialized * For PHP 5.2 compat, since DateTime objects can't be serialized
* @return array * @return array

View File

@@ -36,9 +36,7 @@ class ActionScheduler_IntervalSchedule implements ActionScheduler_Schedule {
} }
/** /**
* @param DateTime $after * @return int
*
* @return DateTime|null
*/ */
public function interval_in_seconds() { public function interval_in_seconds() {
return $this->interval_in_seconds; return $this->interval_in_seconds;

View File

@@ -222,9 +222,16 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
*/ */
protected function get_recurrence( $action ) { protected function get_recurrence( $action ) {
$recurrence = $action->get_schedule(); $recurrence = $action->get_schedule();
if ( $recurrence->is_recurring() ) {
if ( method_exists( $recurrence, 'interval_in_seconds' ) ) { if ( method_exists( $recurrence, 'interval_in_seconds' ) ) {
return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence->interval_in_seconds() ) ); return sprintf( __( 'Every %s', 'action-scheduler' ), self::human_interval( $recurrence->interval_in_seconds() ) );
} }
if ( method_exists( $recurrence, 'get_recurrence' ) ) {
return sprintf( __( 'Cron %s', 'action-scheduler' ), $recurrence->get_recurrence() );
}
}
return __( 'Non-repeating', 'action-scheduler' ); return __( 'Non-repeating', 'action-scheduler' );
} }
@@ -280,7 +287,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) { protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) {
$date = $log_entry->get_date(); $date = $log_entry->get_date();
$date->setTimezone( $timezone ); $date->setTimezone( $timezone );
return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s e' ) ), esc_html( $log_entry->get_message() ) ); return sprintf( '<li><strong>%s</strong><br/>%s</li>', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) );
} }
/** /**
@@ -378,7 +385,7 @@ class ActionScheduler_ListTable extends ActionScheduler_Abstract_ListTable {
$next_timestamp = $schedule->next()->getTimestamp(); $next_timestamp = $schedule->next()->getTimestamp();
$schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s e' ); $schedule_display_string .= $schedule->next()->format( 'Y-m-d H:i:s O' );
$schedule_display_string .= '<br/>'; $schedule_display_string .= '<br/>';
if ( gmdate( 'U' ) > $next_timestamp ) { if ( gmdate( 'U' ) > $next_timestamp ) {

View File

@@ -73,7 +73,7 @@ abstract class ActionScheduler_Logger {
$this->log( $action_id, __( 'action complete', 'action-scheduler' ) ); $this->log( $action_id, __( 'action complete', 'action-scheduler' ) );
} }
public function log_failed_action( $action_id, \Exception $exception ) { public function log_failed_action( $action_id, Exception $exception ) {
$this->log( $action_id, sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() ) ); $this->log( $action_id, sprintf( __( 'action failed: %s', 'action-scheduler' ), $exception->getMessage() ) );
} }

View File

@@ -18,13 +18,6 @@ class ActionScheduler_QueueCleaner {
*/ */
private $month_in_seconds = 2678400; private $month_in_seconds = 2678400;
/**
* Five minutes in seconds
*
* @var int
*/
private $five_minutes = 300;
/** /**
* ActionScheduler_QueueCleaner constructor. * ActionScheduler_QueueCleaner constructor.
* *
@@ -77,8 +70,16 @@ class ActionScheduler_QueueCleaner {
} }
} }
public function reset_timeouts() { /**
$timeout = apply_filters( 'action_scheduler_timeout_period', $this->five_minutes ); * Unclaim pending actions that have not been run within a given time limit.
*
* When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
* as a parameter is 10x the time limit used for queue processing.
*
* @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes).
*/
public function reset_timeouts( $time_limit = 300 ) {
$timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit );
if ( $timeout < 0 ) { if ( $timeout < 0 ) {
return; return;
} }
@@ -97,8 +98,17 @@ class ActionScheduler_QueueCleaner {
} }
} }
public function mark_failures() { /**
$timeout = apply_filters( 'action_scheduler_failure_period', $this->five_minutes ); * Mark actions that have been running for more than a given time limit as failed, based on
* the assumption some uncatachable and unloggable fatal error occurred during processing.
*
* When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed
* as a parameter is 10x the time limit used for queue processing.
*
* @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes).
*/
public function mark_failures( $time_limit = 300 ) {
$timeout = apply_filters( 'action_scheduler_failure_period', $time_limit );
if ( $timeout < 0 ) { if ( $timeout < 0 ) {
return; return;
} }
@@ -119,12 +129,13 @@ class ActionScheduler_QueueCleaner {
/** /**
* Do all of the cleaning actions. * Do all of the cleaning actions.
* *
* @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes).
* @author Jeremy Pry * @author Jeremy Pry
*/ */
public function clean() { public function clean( $time_limit = 300 ) {
$this->delete_old_actions(); $this->delete_old_actions();
$this->reset_timeouts(); $this->reset_timeouts( $time_limit );
$this->mark_failures(); $this->mark_failures( $time_limit );
} }
/** /**

View File

@@ -27,7 +27,8 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu
*/ */
public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) { public function __construct( ActionScheduler_Store $store = null, ActionScheduler_FatalErrorMonitor $monitor = null, ActionScheduler_QueueCleaner $cleaner = null ) {
if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { if ( ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
throw new Exception( __( 'The ' . __CLASS__ . ' class can only be run within WP CLI.', 'action-scheduler' ) ); /* translators: %s php class name */
throw new Exception( sprintf( __( 'The %s class can only be run within WP CLI.', 'action-scheduler' ), __CLASS__ ) );
} }
parent::__construct( $store, $monitor, $cleaner ); parent::__construct( $store, $monitor, $cleaner );
@@ -76,7 +77,7 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu
*/ */
protected function add_hooks() { protected function add_hooks() {
add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) ); add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) );
add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ) ); add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 );
add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 ); add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 );
} }
@@ -143,11 +144,16 @@ class ActionScheduler_WPCLI_QueueRunner extends ActionScheduler_Abstract_QueueRu
* *
* @author Jeremy Pry * @author Jeremy Pry
* *
* @param $action_id * @param int $action_id
* @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility.
*/ */
public function after_execute( $action_id ) { public function after_execute( $action_id, $action = null ) {
// backward compatibility
if ( null === $action ) {
$action = $this->store->fetch_action( $action_id );
}
/* translators: %s refers to the action ID */ /* translators: %s refers to the action ID */
WP_CLI::log( sprintf( __( 'Completed processing action %s', 'action-scheduler' ), $action_id ) ); WP_CLI::log( sprintf( __( 'Completed processing action %s with hook: %s', 'action-scheduler' ), $action_id, $action->get_hook() ) );
} }
/** /**

View File

@@ -0,0 +1,129 @@
<?php
/**
* Class ActionScheduler_wcSystemStatus
*/
class ActionScheduler_wcSystemStatus {
/**
* The active data stores
*
* @var ActionScheduler_Store
*/
protected $store;
function __construct( $store ) {
$this->store = $store;
}
/**
* Display action data, including number of actions grouped by status and the oldest & newest action in each status.
*
* Helpful to identify issues, like a clogged queue.
*/
public function print() {
$action_counts = $this->store->action_counts();
$status_labels = $this->store->get_status_labels();
$oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) );
$this->get_template( $status_labels, $action_counts, $oldest_and_newest );
}
/**
* Get oldest and newest scheduled dates for a given set of statuses.
*
* @param array $status_keys Set of statuses to find oldest & newest action for.
* @return array
*/
protected function get_oldest_and_newest( $status_keys ) {
$oldest_and_newest = array();
foreach ( $status_keys as $status ) {
$oldest_and_newest[ $status ] = array(
'oldest' => '&ndash;',
'newest' => '&ndash;',
);
if ( 'in-progress' === $status ) {
continue;
}
$oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' );
$oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' );
}
return $oldest_and_newest;
}
/**
* Get oldest or newest scheduled date for a given status.
*
* @param string $status Action status label/name string.
* @param string $date_type Oldest or Newest.
* @return DateTime
*/
protected function get_action_status_date( $status, $date_type = 'oldest' ) {
$order = 'oldest' === $date_type ? 'ASC' : 'DESC';
$action = $this->store->query_actions( array(
'claimed' => false,
'status' => $status,
'per_page' => 1,
'order' => $order,
) );
if ( ! empty( $action ) ) {
$date_object = $this->store->get_date( $action[0] );
$action_date = $date_object->format( 'Y-m-d H:i:s O' );
} else {
$action_date = '&ndash;';
}
return $action_date;
}
/**
* Get oldest or newest scheduled date for a given status.
*
* @param array $status_labels Set of statuses to find oldest & newest action for.
* @param array $action_counts Number of actions grouped by status.
* @param array $oldest_and_newest Date of the oldest and newest action with each status.
*/
protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) {
?>
<table class="wc_status_table widefat" cellspacing="0">
<thead>
<tr>
<th colspan="5" data-export-label="Action Scheduler"><h2><?php esc_html_e( 'Action Scheduler', 'action-scheduler' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows scheduled action counts.', 'action-scheduler' ) ); ?></h2></th>
</tr>
<tr>
<td><strong><?php esc_html_e( 'Action Status', 'action-scheduler' ); ?></strong></td>
<td class="help">&nbsp;</td>
<td><strong><?php esc_html_e( 'Count', 'action-scheduler' ); ?></strong></td>
<td><strong><?php esc_html_e( 'Oldest Scheduled Date', 'action-scheduler' ); ?></strong></td>
<td><strong><?php esc_html_e( 'Newest Scheduled Date', 'action-scheduler' ); ?></strong></td>
</tr>
</thead>
<tbody>
<?php
foreach ( $action_counts as $status => $count ) {
// WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column.
printf(
'<tr><td>%1$s</td><td>&nbsp;</td><td>%2$s<span style="display: none;">, Oldest: %3$s, Newest: %4$s</span></td><td>%3$s</td><td>%4$s</td></tr>',
esc_html( $status_labels[ $status ] ),
number_format_i18n( $count ),
$oldest_and_newest[ $status ]['oldest'],
$oldest_and_newest[ $status ]['newest']
);
}
?>
</tbody>
</table>
<?php
}
}

View File

@@ -42,8 +42,10 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
protected function save_post_array( $post_array ) { protected function save_post_array( $post_array ) {
add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
$post_id = wp_insert_post($post_array); $post_id = wp_insert_post($post_array);
remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
if ( is_wp_error($post_id) || empty($post_id) ) { if ( is_wp_error($post_id) || empty($post_id) ) {
throw new RuntimeException(__('Unable to save action.', 'action-scheduler')); throw new RuntimeException(__('Unable to save action.', 'action-scheduler'));
@@ -61,6 +63,41 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
return $postdata; return $postdata;
} }
/**
* Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug().
*
* When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish'
* or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug()
* function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing
* post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a
* post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a
* database containing thousands of related post_name values.
*
* WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue.
*
* We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This
* method is available to be used as a callback on that filter. It provides a more scalable approach to generating a
* post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an
* action's slug, being probably unique is good enough.
*
* For more backstory on this issue, see:
* - https://github.com/Prospress/action-scheduler/issues/44 and
* - https://core.trac.wordpress.org/ticket/21112
*
* @param string $override_slug Short-circuit return value.
* @param string $slug The desired slug (post_name).
* @param int $post_ID Post ID.
* @param string $post_status The post status.
* @param string $post_type Post type.
* @return string
*/
public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) {
if ( self::POST_TYPE == $post_type ) {
$override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false );
}
return $override_slug;
}
protected function save_post_schedule( $post_id, $schedule ) { protected function save_post_schedule( $post_id, $schedule ) {
update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule ); update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule );
} }
@@ -102,7 +139,7 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
} }
$schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true ); $schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true );
if ( empty($schedule) ) { if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) {
$schedule = new ActionScheduler_NullSchedule(); $schedule = new ActionScheduler_NullSchedule();
} }
$group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array('fields' => 'names') ); $group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array('fields' => 'names') );
@@ -404,7 +441,9 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'action-scheduler'), $action_id)); throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'action-scheduler'), $action_id));
} }
do_action( 'action_scheduler_canceled_action', $action_id ); do_action( 'action_scheduler_canceled_action', $action_id );
add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
wp_trash_post($action_id); wp_trash_post($action_id);
remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
} }
public function delete_action( $action_id ) { public function delete_action( $action_id ) {
@@ -587,16 +626,9 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
'ID' => 'ASC', 'ID' => 'ASC',
), ),
'date_query' => array( 'date_query' => array(
'column' => 'post_date', 'column' => 'post_date_gmt',
array( 'before' => $date->format( 'Y-m-d H:i' ),
'compare' => '<=', 'inclusive' => true,
'year' => $date->format( 'Y' ),
'month' => $date->format( 'n' ),
'day' => $date->format( 'j' ),
'hour' => $date->format( 'G' ),
'minute' => $date->format( 'i' ),
'second' => $date->format( 's' ),
),
), ),
'tax_query' => array( 'tax_query' => array(
array( array(
@@ -685,7 +717,7 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
$status = $this->get_post_column( $action_id, 'post_status' ); $status = $this->get_post_column( $action_id, 'post_status' );
if ( $status === null ) { if ( $status === null ) {
throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) ); throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'action-scheduler' ) );
} }
return $this->get_action_status_by_post_status( $status ); return $this->get_action_status_by_post_status( $status );
@@ -716,11 +748,13 @@ class ActionScheduler_wpPostStore extends ActionScheduler_Store {
throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'action-scheduler'), $action_id)); throw new InvalidArgumentException(sprintf(__('Unidentified action %s', 'action-scheduler'), $action_id));
} }
add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 );
add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 );
$result = wp_update_post(array( $result = wp_update_post(array(
'ID' => $action_id, 'ID' => $action_id,
'post_status' => 'publish', 'post_status' => 'publish',
), TRUE); ), TRUE);
remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 );
remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 );
if ( is_wp_error($result) ) { if ( is_wp_error($result) ) {
throw new RuntimeException($result->get_error_message()); throw new RuntimeException($result->get_error_message());
} }

View File

@@ -0,0 +1 @@
actionscheduler.org

View File

@@ -0,0 +1,7 @@
title: Action Scheduler - Job Queue for WordPress
description: A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required.
theme: jekyll-theme-hacker
permalink: /:slug/
plugins:
- jekyll-seo-tag
- jekyll-sitemap

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#63c0f5">
<meta name="msapplication-TileColor" content="#151515">
<meta name="theme-color" content="#ffffff">
{% seo %}
</head>
<body>
<header>
<div class="container">
<p><a href="/usage/">Usage</a> | <a href="/admin/">Admin</a> | <a href="/wp-cli/">WP-CLI</a> | <a href="/perf/">Background Processing at Scale</a> | <a href="/api/">API</a> | <a href="/faq/">FAQ</a>
<h1><a href="/">action-scheduler</a></h1>
<h2>A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required.</h2>
</div>
</header>
<div class="container">
<section id="main_content">
{{ content }}
</section>
</div>
<footer>
<div class="container">
<p><a href="/usage/">Usage</a> | <a href="/admin/">Admin</a> | <a href="/wp-cli/">WP-CLI</a> | <a href="/perf/">Background Processing at Scale</a> | <a href="/api/">API</a> | <a href="/faq/">FAQ</a>
<p class="footer-image">
<a href="https://prospress.com"><img src="http://pic.pros.pr/eb8dcec9bd54/prospress-hacker-green-logo.png" width="120"></a>
</p>
</div>
</footer>
{% if site.google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}
</body>
</html>

View File

@@ -0,0 +1,22 @@
---
description: Learn how to administer background jobs with the Action Scheduler job queue for WordPress.
---
# Scheduled Actions Administration Screen
Action Scheduler has a built in administration screen for monitoring, debugging and manually triggering scheduled actions.
The administration interface is accesible through both:
1. **Tools > Scheduled Actions**
1. **WooCommerce > Status > Scheduled Actions**, when WooCommerce is installed.
Among other tasks, from the admin screen you can:
* run a pending action
* view the scheduled actions with a specific status, like the all actions which have failed or are in-progress (https://cldup.com/NNTwE88Xl8.png).
* view the log entries for a specific action to find out why it failed.
* sort scheduled actions by hook name, scheduled date, claim ID or group name.
Still have questions? Check out the [FAQ](/faq).
![](https://cldup.com/5BA2BNB1sw.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,179 @@
---
description: Reference guide for background processing functions provided by the Action Scheduler job queue for WordPress.
---
# API Reference
Action Scheduler provides a range of functions for scheduling hooks to run at some time in the future on one or more occassions.
To understand the scheduling functoins, it can help to think of them as extensions to WordPress' `do_action()` function that add the ability to delay and repeat when the hook will be triggered.
## WP-Cron APIs vs. Action Scheduler APIs
The Action Scheduler API functions are designed to mirror the WordPress [WP-Cron API functions](http://codex.wordpress.org/Category:WP-Cron_Functions).
Functions return similar values and accept similar arguments to their WP-Cron counterparts. The notable differences are:
* `as_schedule_single_action()` & `as_schedule_recurring_action()` will return the post ID of the scheduled action rather than boolean indicating whether the event was scheduled
* `as_schedule_recurring_action()` takes an interval in seconds as the recurring interval rather than an arbitrary string
* `as_schedule_single_action()` & `as_schedule_recurring_action()` can accept a `$group` parameter to group different actions for the one plugin together.
* the `wp_` prefix is substituted with `as_` and the term `event` is replaced with `action`
## API Function Availability
As mentioned in the [Usage - Load Order](/usage/#load-order) section, Action Scheduler will initialize itself on the `'init'` hook with priority `1`. While API functions are loaded prior to this and call be called, they should not be called until after `'init'` with priority `1`, because each component, like the data store, has not yet been initialized.
Do not use Action Scheduler API functions prior to `'init'` hook with priority `1`. Doing so could lead to unexpected results, like data being stored in the incorrect location.
## Function Reference / `as_schedule_single_action()`
### Description
Schedule an action to run one time.
### Usage
```php
as_schedule_single_action( $timestamp, $hook, $args, $group )
````
### Parameters
- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_.
- **$hook** (string)(required) Name of the action hook. Default: _none_.
- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
- **$group** (array) The group to assign this job to. Default: _''_.
### Return value
(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table.
## Function Reference / `as_schedule_recurring_action()`
### Description
Schedule an action to run repeatedly with a specified interval in seconds.
### Usage
```php
as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group )
````
### Parameters
- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_.
- **$interval_in_seconds** (integer)(required) How long to wait between runs. Default: _none_.
- **$hook** (string)(required) Name of the action hook. Default: _none_.
- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
- **$group** (array) The group to assign this job to. Default: _''_.
### Return value
(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table.
## Function Reference / `as_schedule_cron_action()`
### Description
Schedule an action that recurs on a cron-like schedule.
### Usage
```php
as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group )
````
### Parameters
- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. Default: _none_.
- **$schedule** (string)(required) $schedule A cron-link schedule string, see http://en.wikipedia.org/wiki/Cron. Default: _none_.
- **$hook** (string)(required) Name of the action hook. Default: _none_.
- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
- **$group** (array) The group to assign this job to. Default: _''_.
### Return value
(integer) the action's ID in the [posts](http://codex.wordpress.org/Database_Description#Table_Overview) table.
## Function Reference / `as_unschedule_action()`
### Description
Cancel the next occurrence of a job.
### Usage
```php
as_unschedule_action( $hook, $args, $group )
````
### Parameters
- **$hook** (string)(required) Name of the action hook. Default: _none_.
- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
- **$group** (array) The group to assign this job to. Default: _''_.
### Return value
(null)
## Function Reference / `as_next_scheduled_action()`
### Description
Returns the next timestamp for a scheduled action.
### Usage
```php
as_next_scheduled_action( $hook, $args, $group )
````
### Parameters
- **$hook** (string)(required) Name of the action hook. Default: _none_.
- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_.
- **$group** (array) The group to assign this job to. Default: _''_.
### Return value
(integer|boolean) The timestamp for the next occurrence, or false if nothing was found.
## Function Reference / `as_get_scheduled_actions()`
### Description
Find scheduled actions.
### Usage
```php
as_get_scheduled_actions( $args, $return_format )
````
### Parameters
- **$args** (array) Arguments to search and filter results by. Possible arguments, with their default values:
* `'hook' => ''` - 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().
* `'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().
* `'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'`
- **$return_format** (string) The format in which to return the scheduled actions: 'OBJECT', 'ARRAY_A', or 'ids'. Default: _'OBJECT'_.
### Return value
(array) Array of the actions matching the criteria specified with `$args`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,32 @@
---
---
@import "{{ site.theme }}";
a {
text-shadow: none;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
header h1 a {
color: #b5e853;
}
.container {
max-width: 700px;
}
footer {
margin-top: 6em;
padding: 1.6em 0;
border-top: 1px dashed #b5e853;
}
.footer-image {
text-align: center;
padding-top: 1em;
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#151515</TileColor>
</tile>
</msapplication>
</browserconfig>

View File

@@ -0,0 +1,101 @@
## FAQ
### Is it safe to release Action Scheduler in my plugin? Won't its functions conflict with another copy of the library?
Action Scheduler is designed to be used and released in plugins. It avoids redeclaring public API functions when more than one copy of the library is being loaded by different plugins. It will also load only the most recent version of itself (by checking registered versions after all plugins are loaded on the `'plugins_loaded'` hook).
To use it in your plugin, simply require the `action-scheduler/action-scheduler.php` file. Action Scheduler will take care of the rest.
### I don't want to use WP-Cron. Does Action Scheduler depend on WP-Cron?
By default, Action Scheduler is initiated by WP-Cron. However, it has no dependency on the WP-Cron system. You can initiate the Action Scheduler queue in other ways with just one or two lines of code.
For example, you can start a queue directly by calling:
```php
ActionScheduler::runner()->run();
```
Or trigger the `'action_scheduler_run_queue'` hook and let Action Scheduler do it for you:
```php
do_action( 'action_scheduler_run_queue' );
```
Further customization can be done by extending the `ActionScheduler_Abstract_QueueRunner` class to create a custom Queue Runner. For an example of a customized queue runner, see the [`ActionScheduler_WPCLI_QueueRunner`](https://github.com/Prospress/action-scheduler/blob/master/classes/ActionScheduler_WPCLI_QueueRunner.php), which is used when running WP CLI.
Want to create some other method for initiating Action Scheduler? [Open a new issue](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.
### I don't want to use WP-Cron, ever. Does Action Scheduler replace WP-Cron?
By default, Action Scheduler is designed to work alongside WP-Cron and not change any of its behaviour. This helps avoid unexpectedly overriding WP-Cron on sites installing your plugin, which may have nothing to do with WP-Cron.
However, we can understand why you might want to replace WP-Cron completely in environments within you control, especially as it gets you the advantages of Action Scheduler. This should be possible without too much code.
You could use the `'schedule_event'` hook in WordPress to use Action Scheduler for only newly scheduled WP-Cron jobs and map the `$event` param to Action Scheduler API functions.
Alternatively, you can use a combination of the `'pre_update_option_cron'` and `'pre_option_cron'` hooks to override all new and previously scheduled WP-Cron jobs (similar to the way [Cavalcade](https://github.com/humanmade/Cavalcade) does it).
If you'd like to create a plugin to do this automatically and want to share your work with others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.
### Eww gross, Custom Post Types! That's _so_ 2010. Can I use a different storage scheme?
Of course! Action Scheduler data storage is completely swappable, and always has been.
You can store scheduled actions in custom tables in the WordPress site's database. Some sites using it already are. You can actually store them anywhere for that matter, like in a remote storage service from Amazon Web Services.
To implement a custom store:
1. extend the abstract `ActionScheduler_Store` class, being careful to implement each of its methods
2. attach a callback to `'action_scheduler_store_class'` to tell Action Scheduler your class is the one which should be used to manage storage, e.g.
```
function eg_define_custom_store( $existing_storage_class ) {
return 'My_Radical_Action_Scheduler_Store';
}
add_filter( 'action_scheduler_store_class', 'eg_define_custom_store', 10, 1 );
```
Take a look at the `ActionScheduler_wpPostStore` class for an example implementation of `ActionScheduler_Store`.
If you'd like to create a plugin to do this automatically and release it publicly to help others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.
> Note: we're also moving Action Scheduler itself to use [custom tables for better scalability](https://github.com/Prospress/action-scheduler/issues/77).
### Can I use a different storage scheme just for logging?
Of course! Action Scheduler's logger is completely swappable, and always has been. You can also customise where logs are stored, and the storage mechanism.
To implement a custom logger:
1. extend the abstract `ActionScheduler_Logger` class, being careful to implement each of its methods
2. attach a callback to `'action_scheduler_logger_class'` to tell Action Scheduler your class is the one which should be used to manage logging, e.g.
```
function eg_define_custom_logger( $existing_storage_class ) {
return 'My_Radical_Action_Scheduler_Logger';
}
add_filter( 'action_scheduler_logger_class', 'eg_define_custom_logger', 10, 1 );
```
Take a look at the `ActionScheduler_wpCommentLogger` class for an example implementation of `ActionScheduler_Logger`.
### I want to run Action Scheduler only on a dedicated application server in my cluster. Can I do that?
Wow, now you're really asking the tough questions. In theory, yes, this is possible. The `ActionScheduler_QueueRunner` class, which is responsible for running queues, is swappable via the `'action_scheduler_queue_runner_class'` filter.
Because of this, you can effectively customise queue running however you need. Whether that means tweaking minor things, like not using WP-Cron at all to initiate queues by overriding `ActionScheduler_QueueRunner::init()`, or completely changing how and where queues are run, by overriding `ActionScheduler_QueueRunner::run()`.
### Is Action Scheduler safe to use on my production site?
Yes, absolutely! Action Scheduler is actively used on tens of thousands of production sites already. Right now it's responsible for scheduling everything from emails to payments.
In fact, every month, Action Scheduler processes millions of payments as part of the [WooCommerce Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/) extension.
It requires no setup, and won't override any WordPress APIs (unless you want it to).
### How does Action Scheduler work on WordPress Multisite?
Action Scheduler is designed to manage the scheduled actions on a single site. It has no special handling for running queues across multiple sites in a multisite network. That said, because it's storage and Queue Runner are completely swappable, it would be possible to write multisite handling classes to use with it.
If you'd like to create a multisite plugin to do this and release it publicly to help others, [open a new issue to let us know](https://github.com/Prospress/action-scheduler/issues/new), we'd love to help you with it.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
google-site-verification: google14ef723abb376cd3.html

View File

@@ -0,0 +1,68 @@
---
title: Action Scheduler - Background Processing Job Queue for WordPress
---
## WordPress Job Queue with Background Processing
Action Scheduler is a library for triggering a WordPress hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occassions.
Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook.
It just so happens, this functionality also creates a robust job queue for background processing large queues of tasks in WordPress. With the additional of logging and an [administration interface](/admin/), that also provide tracability on your tasks processed in the background.
### Battle-Tested Background Processing
Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins.
It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, in 10 concurrent queues at a rate of over 10,000 actions / hour without negatively impacting normal site operations.
This is all possible on infrastructure and WordPress sites outside the control of the plugin author.
Action Scheduler is specifically designed for distribution in WordPress plugins (and themes) - no server access required. If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help.
### How it Works
Action Scheduler uses a WordPress [custom post type](http://codex.wordpress.org/Post_Types), creatively named `scheduled-action`, to store the hook name, arguments and scheduled date for an action that should be triggered at some time in the future.
The scheduler will attempt to run every minute by attaching itself as a callback to the `'action_scheduler_run_schedule'` hook, which is scheduled using WordPress's built-in [WP-Cron](http://codex.wordpress.org/Function_Reference/wp_cron) system.
When triggered, Action Scheduler will check for posts of the `scheduled-action` type that have a `post_date` at or before this point in time i.e. actions scheduled to run now or at sometime in the past.
### Batch Processing Background Jobs
If there are actions to be processed, Action Scheduler will stake a unique claim for a batch of 20 actions and begin processing that batch. The PHP process spawned to run the batch will then continue processing batches of 20 actions until it times out or exhausts available memory.
If your site has a large number of actions scheduled to run at the same time, Action Scheduler will process more than one batch at a time. Specifically, when the `'action_scheduler_run_schedule'` hook is triggered approximately one minute after the first batch began processing, a new PHP process will stake a new claim to a batch of actions which were not claimed by the previous process. It will then begin to process that batch.
This will continue until all actions are processed using a maximum of 5 concurrent queues.
### Housekeeping
Before processing a batch, the scheduler will remove any existing claims on actions which have been sitting in a queue for more than five minutes.
Action Scheduler will also trash any actions which were completed more than a month ago.
If an action runs for more than 5 minutes, Action Scheduler will assume the action has timed out and will mark it as failed. However, if all callbacks attached to the action were to successfully complete sometime after that 5 minute timeout, its status would later be updated to completed.
### Traceable Background Processing
Did your background job run?
Never be left wondering with Action Scheduler's built-in record keeping.
All events for each action are logged in the [comments table](http://codex.wordpress.org/Database_Description#Table_Overview) and displayed in the [administration interface](/admin/).
The events logged by default include when an action:
* is created
* starts
* completes
* fails
If it fails with an error that can be recorded, that error will be recorded in the log and visible in administration interface, making it possible to trace what went wrong at some point in the past on a site you didn't have access to in the past.
Actions can also be grouped together using a custom taxonomy named `action-group`.
## Credits
Developed and maintained by [Prospress](http://prospress.com/) in collaboration with [Flightless](https://flightless.us/).
Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/prospress/action-scheduler/pulls) welcome.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,127 @@
---
title: WordPress Background Processing at Scale - Action Scheduler Job Queue
description: Learn how to do WordPress background processing at scale by tuning the Action Scheduler job queue's default WP Cron runner.
---
# Background Processing at Scale
Action Scheduler's default processing is designed to work reliably across all different hosting environments. In order to achieve that, the default processing thresholds are very conservative.
Specifically, Action Scheduler will only process actions until:
* 90% of available memory is used
* processing another 3 actions would exceed 30 seconds of total request time, based on the average processing time for the current batch
On sites with large queues, this can result in very slow processing time.
While using [WP CLI to process queues](/wp-cli/) is the best approach to increasing processing speed, on occasion, that is not a viable option. In these cases, it's also possible to increase the processing thresholds in Action Scheduler to increase the rate at which actions are processed by the default WP Cron queue runner.
## Increasing Time Limit
By default, Action Scheduler will only process actions for a maximum of 30 seconds. This time limit minimises the risk of a script timeout on unknown hosting environments, some of which enforce 30 second timeouts.
If you know your host supports longer than this time limit for web requests, you can increase this time limit. This allows more actions to be processed in each request and reduces the lag between processing each queue, greating speeding up the processing rate of scheduled actions.
For example, the following snippet will increase the timelimit to 2 minutes (120 seconds):
```php
function eg_increase_time_limit( $time_limit ) {
return 120;
}
add_filter( 'action_scheduler_queue_runner_time_limit', 'eg_increase_time_limit' );
```
Some of the known host time limits are:
* 60 second on WP Engine
* 120 seconds on Pantheon
* 120 seconds on SiteGround
## Increasing Batch Size
By default, Action Scheduler will claim a batch of 25 actions. This small batch size is because the default time limit is only 30 seconds; however, if you know your actions are processing very quickly, e.g. taking microseconds not seconds, or that you have more than 30 second available to process each batch, increasing the batch size can improve performance.
This is because claiming a batch has some overhead, so the less often a batch needs to be claimed, the faster actions can be processed.
For example, to increase the batch size to 100, we can use the following function:
```php
function eg_increase_action_scheduler_batch_size( $batch_size ) {
return 100;
}
add_filter( 'action_scheduler_queue_runner_batch_size', 'eg_increase_action_scheduler_batch_size' );
```
## Increasing Concurrent Batches
By default, Action Scheduler will run up to 5 concurrent batches of actions. This is to prevent consuming all the available connections or processes on your webserver.
However, your server may allow a large number of connection, for example, because it has a high value for Apache's `MaxClients` setting or PHP-FPM's `pm.max_children` setting.
If this is the case, you can use the `'action_scheduler_queue_runner_concurrent_batches'` filter to increase the number of conncurrent batches allowed, and therefore speed up processing large numbers of actions scheduled to be processed simultaneously.
For example, to increase the allowed number of concurrent queues to 10, we can use the following code:
```php
function eg_increase_action_scheduler_concurrent_batches( $concurrent_batches ) {
return 10;
}
add_filter( 'action_scheduler_queue_runner_concurrent_batches', 'eg_increase_action_scheduler_concurrent_batches' );
```
## Increasing Initialisation Rate of Runners
By default, Action scheduler initiates at most, one queue runner every time the `'action_scheduler_run_queue'` action is triggered by WP Cron.
Because this action is only triggered at most once every minute, if a queue is only allowed to process for one minute, then there will never be more than one queue processing actions, greatly reducing the processing rate.
To handle larger queues on more powerful servers, it's a good idea to initiate additional queue runners whenever the `'action_scheduler_run_queue'` action is run.
That can be done by initiated additional secure requests to our server via loopback requests.
The code below demonstrates how to create 5 loopback requests each time a queue begins
```php
/**
* Trigger 5 additional loopback requests with unique URL params.
*/
function eg_request_additional_runners() {
// allow self-signed SSL certificates
add_filter( 'https_local_ssl_verify', '__return_false', 100 );
for ( $i = 0; $i < 5; $i++ ) {
$response = wp_remote_post( admin_url( 'admin-ajax.php' ), array(
'method' => 'POST',
'timeout' => 45,
'redirection' => 5,
'httpversion' => '1.0',
'blocking' => false,
'headers' => array(),
'body' => array(
'action' => 'eg_create_additional_runners',
'instance' => $i,
'eg_nonce' => wp_create_nonce( 'eg_additional_runner_' . $i ),
),
'cookies' => array(),
) );
}
}
add_action( 'action_scheduler_run_queue', 'eg_request_additional_runners', 0 );
/**
* Handle requests initiated by eg_request_additional_runners() and start a queue runner if the request is valid.
*/
function eg_create_additional_runners() {
if ( isset( $_POST['eg_nonce'] ) && isset( $_POST['instance'] ) && wp_verify_nonce( $_POST['eg_nonce'], 'eg_additional_runner_' . $_POST['instance'] ) ) {
ActionScheduler_QueueRunner::instance()->run();
}
wp_die();
}
add_action( 'wp_ajax_nopriv_eg_create_additional_runners', 'eg_create_additional_runners', 0 );
```
## High Volume Plugin
It's not necessary to add all of this code yourself, the folks at [Prospress](https://prospress.com) have created a handy plugin to get access to each of these increases - the [Action Scheduler - High Volume](https://github.com/prospress/action-scheduler-high-volume) plugin.

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="312.000000pt" height="312.000000pt" viewBox="0 0 312.000000 312.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,312.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1837 2924 c-1 -1 -54 -5 -117 -8 -63 -4 -128 -9 -145 -11 -16 -2
-46 -6 -65 -9 -19 -3 -57 -8 -85 -11 -27 -3 -70 -10 -95 -16 -25 -5 -58 -11
-75 -13 -98 -13 -360 -73 -565 -130 -236 -65 -584 -172 -609 -187 -105 -67 24
-294 221 -387 77 -37 152 -61 206 -65 16 -1 22 -8 22 -23 0 -34 35 -107 70
-147 62 -71 161 -119 265 -130 34 -4 40 -8 46 -33 25 -122 151 -205 301 -198
122 5 254 44 270 80 6 13 8 27 5 30 -3 4 -41 1 -84 -5 -119 -18 -155 -19 -217
-9 -74 11 -123 47 -132 94 -7 40 -4 46 28 49 94 12 238 52 260 72 35 32 23 46
-34 38 -29 -4 -62 -9 -73 -11 -45 -10 -255 -11 -320 -2 -81 11 -154 40 -185
73 -24 26 -52 97 -42 106 4 4 45 9 92 12 93 7 90 6 269 43 69 14 134 30 145
36 29 15 56 39 56 50 0 10 -74 3 -144 -12 -22 -5 -144 -8 -270 -6 -251 3 -327
15 -443 71 -47 23 -133 97 -133 115 0 6 150 49 225 64 11 3 83 20 160 39 77
19 154 38 170 41 17 4 39 8 50 10 11 3 56 12 100 21 98 21 104 22 146 29 19 3
43 7 54 10 28 5 94 16 130 20 17 3 44 7 60 10 17 2 50 7 75 11 41 5 87 11 150
20 111 16 452 23 509 11 14 -3 47 -8 75 -11 28 -3 78 -13 111 -21 365 -92 513
-289 509 -679 -1 -102 -35 -265 -56 -265 -5 0 -7 -4 -4 -9 3 -4 -12 -41 -33
-81 -144 -277 -468 -551 -794 -671 -107 -39 -317 -88 -317 -73 0 2 50 118 111
257 61 138 131 299 156 357 25 58 49 114 54 125 5 11 52 119 104 240 90 208
94 221 84 255 -19 58 -67 147 -99 183 -33 38 -86 65 -99 51 -5 -5 -23 -43 -41
-84 -50 -116 -179 -408 -211 -478 -16 -35 -29 -65 -29 -67 0 -2 -18 -43 -40
-90 -22 -47 -40 -88 -40 -90 0 -4 -92 -211 -156 -353 -13 -29 -24 -55 -24 -57
0 -2 -15 -37 -34 -77 -18 -40 -46 -102 -61 -138 -16 -36 -45 -102 -67 -148
-21 -46 -38 -85 -38 -87 0 -2 -34 -81 -76 -175 -55 -124 -74 -178 -70 -196 16
-62 116 -134 207 -148 36 -5 37 -5 63 53 14 32 51 112 82 177 31 66 74 160 96
209 22 50 43 93 47 98 7 7 19 10 82 21 19 3 44 8 55 10 12 2 38 7 57 10 480
76 883 299 1117 618 100 135 202 363 224 498 1 8 5 35 9 60 16 95 13 216 -7
340 -10 59 -60 190 -97 252 -45 76 -151 190 -219 235 -127 85 -324 159 -475
179 -27 3 -53 8 -56 9 -12 8 -344 25 -352 19z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -0,0 +1,123 @@
---
description: Learn how to use the Action Scheduler background processing job queue for WordPress in your WordPress plugin.
---
# Usage
Using Action Scheduler requires:
1. installing the library
1. scheduling and action
1. attaching a callback to that action
## Scheduling an Action
To schedule an action, call the [API function](/api/) for the desired schedule type passing in the required parameters.
The example code below shows everything needed to schedule a function to run at midnight, if it's not already scheduled:
```php
require_once( plugin_dir_path( __FILE__ ) . '/libraries/action-scheduler/action-scheduler.php' );
/**
* Schedule an action with the hook 'eg_midnight_log' to run at midnight each day
* so that our callback is run then.
*/
function eg_log_action_data() {
if ( false === as_next_scheduled_action( 'eg_midnight_log' ) ) {
as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, 'eg_midnight_log' );
}
}
add_action( 'init', 'eg_log_action_data' );
/**
* A callback to run when the 'eg_midnight_log' scheduled action is run.
*/
function eg_log_action_data() {
error_log( 'It is just after midnight on ' . date( 'Y-m-d' ) );
}
add_action( 'eg_midnight_log', 'eg_log_action_data' );
```
For more details on all available API functions, and the data they accept, refer to the [API Reference](/api/).
## Installation
There are two ways to install Action Scheduler:
1. regular WordPress plugin; or
1. a library within your plugin's codebase.
### Usage as a Plugin
Action Scheduler includes the necessary file headers to be used as a standard WordPress plugin.
To install it as a plugin:
1. Download the .zip archive of the latest [stable release](https://github.com/Prospress/action-scheduler/releases)
1. Go to the **Plugins > Add New > Upload** administration screen on your WordPress site
1. Select the archive file you just downloaded
1. Click **Install Now**
1. Click **Activate**
Or clone the Git repository into your site's `wp-content/plugins` folder.
Using Action Scheduler as a plugin can be handy for developing against newer versions, rather than having to update the subtree in your codebase. **When installed as a plugin, Action Scheduler does not provide any user interfaces for scheduling actions**. The only way to interact with Action Scheduler is via code.
### Usage as a Library
To use Action Scheduler as a library:
1. include the Action Scheduler codebase
1. load the library by including the `action-scheduler.php` file
Using a [subtree in your plugin, theme or site's Git repository](https://www.atlassian.com/blog/git/alternatives-to-git-submodule-git-subtree) to include Action Scheduler is the recommended method. Composer can also be used.
To include Action Scheduler as a git subtree:
#### Step 1. Add the Repository as a Remote
```
git remote add -f subtree-action-scheduler https://github.com/Prospress/action-scheduler.git
```
Adding the subtree as a remote allows us to refer to it in short from via the name `subtree-action-scheduler`, instead of the full GitHub URL.
#### Step 2. Add the Repo as a Subtree
```
git subtree add --prefix libraries/action-scheduler subtree-action-scheduler master --squash
```
This will add the `master` branch of Action Scheduler to your repository in the folder `libraries/action-scheduler`.
You can change the `--prefix` to change where the code is included. Or change the `master` branch to a tag, like `2.1.0` to include only a stable version.
#### Step 3. Update the Subtree
To update Action Scheduler to a new version, use the commands:
```
git fetch subtree-action-scheduler master
git subtree pull --prefix libraries/action-scheduler subtree-action-scheduler master --squash
```
### Loading Action Scheduler
Regardless of how it is installed, to load Action Scheduler, you only need to include the `action-scheduler.php` file, e.g.
```php
<?php
require_once( plugin_dir_path( __FILE__ ) . '/libraries/action-scheduler/action-scheduler.php' );
```
There is no need to call any functions or do else to initialize Action Scheduler.
When the `action-scheduler.php` file is included, Action Scheduler will register the version in that file and then load the most recent version of itself on the site. It will also load the most recent version of [all API functions](https://github.com/prospress/action-scheduler#api-functions).
### Load Order
Action Scheduler will register its version on `'plugins_loaded'` with priority `0` - after all other plugin codebases has been loaded. Therefore **the `action-scheduler.php` file must be included before `'plugins_loaded'` priority `0`**.
It is recommended to load it _when the file including it is included_. However, if you need to load it on a hook, then the hook must occur before `'plugins_loaded'`, or you can use `'plugins_loaded'` with negative priority, like `-10`.
Action Scheduler will later initialize itself on `'init'` with priority `1`. Action Scheduler APIs should not be used until after `'init'` with priority `1`.

View File

@@ -0,0 +1,73 @@
---
description: Learn how to do WordPress background processing at scale with WP CLI and the Action Scheduler job queue.
---
# WP CLI
Action Scheduler has custom [WP CLI](http://wp-cli.org) commands available for processing actions.
For large sites, WP CLI is a much better choice for running queues of actions than the default WP Cron runner. These are some common cases where WP CLI is a better option:
* long-running tasks - Tasks that take a significant amount of time to run
* large queues - A large number of tasks will naturally take a longer time
* other plugins with extensive WP Cron usage - WP Cron's limited resources are spread across more tasks
With a regular web request, you may have to deal with script timeouts enforced by hosts, or other restraints that make it more challenging to run Action Scheduler tasks. Utilizing WP CLI to run commands directly on the server give you more freedom. This means that you typically don't have the same constraints of a normal web request.
If you choose to utilize WP CLI exclusively, you can disable the normal WP CLI queue runner by installing the [Action Scheduler - Disable Default Queue Runner](https://github.com/Prospress/action-scheduler-disable-default-runner) plugin. Note that if you do this, you **must** run Action Scheduler via WP CLI or another method, otherwise no scheduled actions will be processed.
## Commands
These are the commands available to use with Action Scheduler:
* `action-scheduler run`
Options:
* `--batch-size` - This is the number of actions to run in a single batch. The default is `100`.
* `--batches` - This is the number of batches to run. Using 0 means that batches will continue running until there are no more actions to run.
* `--hooks` - Process only actions with specific hook or hooks, like `'woocommerce_scheduled_subscription_payment'`. By default, actions with any hook will be processed. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=woocommerce_scheduled_subscription_trial_end,woocommerce_scheduled_subscription_payment,woocommerce_scheduled_subscription_expiration`
* `--group` - Process only actions in a specific group, like `'woocommerce-memberships'`. By default, actions in any group (or no group) will be processed.
* `--force` - By default, Action Scheduler limits the number of concurrent batches that can be run at once to ensure the server does not get overwhelmed. Using the `--force` flag overrides this behavior to force the WP CLI queue to run.
The best way to get a full list of commands and their available options is to use WP CLI itself. This can be done by running `wp action-scheduler` to list all Action Scheduler commands, or by including the `--help` flag with any of the individual commands. This will provide all relevant parameters and flags for the command.
## Cautionary Note on Action Dependencies when using `--group` or `--hooks` Options
The `--group` and `--hooks` options should be used with caution if you have an implicit dependency between scheduled actions based on their schedule.
For example, consider two scheduled actions for the same subscription:
* `scheduled_payment` scheduled for `2015-11-13 00:00:00` and
* `scheduled_expiration` scheduled for `2015-11-13 00:01:00`.
Under normal conditions, Action Scheduler will ensure the `scheduled_payment` action is run before the `scheduled_expiration` action. Becuase that's how they are scheduled.
However, when using the `--hooks` option, the `scheduled_payment` and `scheduled_expiration` actions will be processed in separate queues. As a result, this dependency is not guaranteed.
For example, consider a site with both:
* 100,000 `scheduled_payment` actions, scheduled for `2015-11-13 00:00:00`
* 100 `scheduled_expiration` actions, scheduled for `2015-11-13 00:01:00`
If two queue runners are running alongside each other with each runner dedicated to just one of these hooks, the queue runner handling expiration hooks will complete the processing of the expiration hooks more quickly than the queue runner handling all the payment actions.
**Because of this, the `--group` and `--hooks` options should be used with caution to avoid processing actions with an implicit dependency based on their schedule in separate queues.**
## Improving Performance with `--group` or `--hooks`
Being able to run queues for specific hooks or groups of actions is valuable at scale. Why? Because it means you can restrict the concurrency for similar actions.
For example, let's say you have 300,000 actions queued up comprised of:
* 100,000 renewals payments
* 100,000 email notifications
* 100,000 membership status updates
Action Scheduler's default WP Cron queue runner will process them all together. e.g. when it claims a batch of actions, some may be emails, some membership updates and some renewals.
When you add concurrency to that, you can end up with issues. For example, if you have 3 queues running, they may all be attempting to process similar actions at the same time, which can lead to querying the same database tables with similar queries. Depending on the code/queries running, this can lead to database locks or other issues.
If you can batch based on each action's group, then you can improve performance by processing like actions consecutively, but still processing the full set of actions concurrently.
For example, if one queue is created to process emails, another to process membership updates, and another to process renewal payments, then the same queries won't be run at the same time, and 3 separate queues will be able to run more efficiently.
The WP CLI runner can achieve this using the `--group` option.

View File

@@ -120,9 +120,12 @@ class WCS_Privacy_Background_Updater {
$batch_size = 20; $batch_size = 20;
// Get the ended_statuses and removes pending-cancel.
$subscription_ended_statuses = array_diff( wcs_get_subscription_ended_statuses(), array( 'pending-cancel' ) );
$subscriptions = wcs_get_subscriptions( array( $subscriptions = wcs_get_subscriptions( array(
'subscriptions_per_page' => $batch_size, 'subscriptions_per_page' => $batch_size,
'subscription_status' => wcs_get_subscription_ended_statuses(), 'subscription_status' => $subscription_ended_statuses,
'meta_query' => array( 'meta_query' => array(
array( array(
'key' => '_schedule_end', 'key' => '_schedule_end',

View File

@@ -101,9 +101,6 @@ 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. // 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 ); 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_add_downgrade_notice' ) );
add_action( 'admin_notices', array( __CLASS__, 'maybe_display_external_object_cache_warning' ) ); add_action( 'admin_notices', array( __CLASS__, 'maybe_display_external_object_cache_warning' ) );
@@ -238,6 +235,11 @@ class WC_Subscriptions_Upgrader {
self::$background_updaters['2.4']['start_date_metadata']->schedule_repair(); self::$background_updaters['2.4']['start_date_metadata']->schedule_repair();
} }
// Upon upgrading or installing 2.5.0 for the first time, enable or disable PayPal Standard for Subscriptions.
if ( version_compare( self::$active_version, '2.5.0', '<' ) ) {
WCS_PayPal::set_enabled_for_subscriptions_default();
}
self::upgrade_complete(); self::upgrade_complete();
} }
@@ -368,8 +370,8 @@ class WC_Subscriptions_Upgrader {
$results = array( $results = array(
'upgraded_count' => 0, 'upgraded_count' => 0,
// translators: 1$: error message, 2$: opening link tag, 3$: closing link tag // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag
'message' => sprintf( __( 'Unable to upgrade subscriptions.<br/>Error: %1$s<br/>Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">', '</a>' ), 'message' => sprintf( __( 'Unable to upgrade subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">', '</a>', '<br />' ),
'status' => 'error', 'status' => 'error',
); );
} }
@@ -415,8 +417,8 @@ class WC_Subscriptions_Upgrader {
$results = array( $results = array(
'repaired_count' => 0, 'repaired_count' => 0,
'unrepaired_count' => 0, 'unrepaired_count' => 0,
// translators: 1$: error message, 2$: opening link tag, 3$: closing link tag // translators: 1$: error message, 2$: opening link tag, 3$: closing link tag, 4$: break tag
'message' => sprintf( _x( 'Unable to repair subscriptions.<br/>Error: %1$s<br/>Please refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">', '</a>' ), 'message' => sprintf( _x( 'Unable to repair subscriptions.%4$sError: %1$s%4$sPlease refresh the page and try again. If problem persists, %2$scontact support%3$s.', 'Error message that gets sent to front end when upgrading Subscriptions', 'woocommerce-subscriptions' ), '<code>' . $e->getMessage(). '</code>', '<a href="' . esc_url( 'https://woocommerce.com/my-account/create-a-ticket/' ) . '">', '</a>', '<br />' ),
'status' => 'error', 'status' => 'error',
); );
} }
@@ -826,24 +828,6 @@ 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. * Load and initialise the background updaters.
* *
@@ -854,7 +838,6 @@ class WC_Subscriptions_Upgrader {
self::$background_updaters['2.3']['suspended_paypal_repair'] = new WCS_Repair_Suspended_PayPal_Subscriptions( $logger ); 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.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']['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 // Init the updaters
foreach ( self::$background_updaters as $version => $updaters ) { foreach ( self::$background_updaters as $version => $updaters ) {
@@ -903,6 +886,27 @@ class WC_Subscriptions_Upgrader {
$admin_notice->display(); $admin_notice->display();
} }
/**
* Handles the WC 3.5.0 upgrade routine that moves customer IDs from post metadata to the 'post_author' column.
*
* @since 2.4.0
* @deprecated 2.5.0
*/
public static function maybe_update_subscription_post_author() {
wcs_deprecated_function( __METHOD__, '2.5.0' );
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();
}
}
/** /**
* Used to check if a user ID is greater than the last user upgraded to version 1.4. * Used to check if a user ID is greater than the last user upgraded to version 1.4.
* *

View File

@@ -25,7 +25,8 @@ class WCS_Upgrade_Logger {
public static function init() { public static function init() {
add_action( 'woocommerce_subscriptions_upgraded', __CLASS__ . '::schedule_cleanup', 10, 2 ); add_action( 'woocommerce_subscriptions_upgraded', array( __CLASS__, 'schedule_cleanup' ), 10, 2 );
add_action( 'woocommerce_subscriptions_upgraded', array( __CLASS__, 'add_more_info' ), 10 );
} }
/** /**
@@ -63,6 +64,34 @@ class WCS_Upgrade_Logger {
} }
} }
/**
* Log more information during upgrade: Information about environment and active plugins
*
* @since 2.5.0
*/
public static function add_more_info() {
global $wp_version;
self::add( sprintf( 'Environment info:' ) );
self::add( sprintf( ' WordPress Version : %s', $wp_version ) );
$active_plugins = get_option( 'active_plugins' );
// Check if get_plugins() function exists. This is required on the front end of the site.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
self::add( sprintf( 'Active Plugins:' ) );
foreach ( $active_plugins as $plugin ) {
$author = empty( $all_plugins[ $plugin ]['Author'] ) ? 'Unknown' : $all_plugins[ $plugin ]['Author'];
$version = empty( $all_plugins[ $plugin ]['Version'] ) ? 'Unknown version' : $all_plugins[ $plugin ]['Version'];
self::add( sprintf( ' %s by %s %s', $all_plugins[ $plugin ]['Name'], $author, $version ) );
}
}
/** /**
* Schedule a hook to automatically clear the log after 8 weeks * Schedule a hook to automatically clear the log after 8 weeks
*/ */
@@ -71,3 +100,4 @@ 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' ) ) ); 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' ) ) );
} }
} }

View File

@@ -19,7 +19,7 @@ class WCS_Upgrade_Notice_Manager {
* *
* @var string * @var string
*/ */
protected static $version = '2.3.0'; protected static $version = '2.5.0';
/** /**
* The number of times the notice will be displayed before being dismissed automatically. * The number of times the notice will be displayed before being dismissed automatically.
@@ -77,26 +77,25 @@ class WCS_Upgrade_Notice_Manager {
return; return;
} }
$version = _x( '2.3', 'plugin version number used in admin notice', 'woocommerce-subscriptions' ); $version = _x( '2.5', 'plugin version number used in admin notice', 'woocommerce-subscriptions' );
$dismiss_url = wp_nonce_url( add_query_arg( 'dismiss_upgrade_notice', self::$version ), 'dismiss_upgrade_notice', '_wcsnonce' ); $dismiss_url = wp_nonce_url( add_query_arg( 'dismiss_upgrade_notice', self::$version ), 'dismiss_upgrade_notice', '_wcsnonce' );
$notice = new WCS_Admin_Notice( 'notice notice-info', array(), $dismiss_url ); $notice = new WCS_Admin_Notice( 'notice notice-info', array(), $dismiss_url );
$features = array( $features = array(
array( array(
'title' => __( 'New Subscription Coupon Features', 'woocommerce-subscriptions' ), 'title' => __( 'New options to allow customers to sign up without a credit card', 'woocommerce-subscriptions' ),
'description' => __( 'Want to offer customers coupons which apply for 6 months? You can now define the number of cycles discounts would be applied.', 'woocommerce-subscriptions' ), 'description' => __( 'Allow customers to access free trial and other $0 subscription products without needing to enter their credit card details on sign up.', 'woocommerce-subscriptions' ),
), ),
array( array(
'title' => __( 'New Signup Pricing Options for Synchronized Subscriptions', 'woocommerce-subscriptions' ), 'title' => __( 'Improved subscription payment method information', 'woocommerce-subscriptions' ),
'description' => __( 'Charge the full recurring price at the time of sign up for synchronized subscriptions. Your customers can now receive their products straight away.', 'woocommerce-subscriptions' ), 'description' => __( 'Customers can now see more information about what payment method will be used for future payments.', 'woocommerce-subscriptions' ),
), ),
array( array(
'title' => __( 'Link Parent Orders to Subscriptions', 'woocommerce-subscriptions' ), 'title' => __( 'Auto-renewal toggle', 'woocommerce-subscriptions' ),
// translators: placeholders are opening and closing <a> tags linking to documentation. 'description' => sprintf( __( 'Enabled via a setting, this new feature will allow your customers to turn on and off automatic payments from the %sMy Account > View Subscription%s pages.', 'woocommerce-subscriptions' ), '<strong>', '</strong>' ),
'description' => sprintf( __( 'For subscriptions with no parent order, shop managers can now choose a parent order via the Edit Subscription screen. This makes it possible to set a parent order on %smanually created subscriptions%s. The order can also be sent to customers to act as an invoice that needs to be paid to activate the subscription.', 'woocommerce-subscriptions' ), '<a href="https://docs.woocommerce.com/document/subscriptions/add-or-modify-a-subscription/">', '</a>' ),
), ),
array( array(
'title' => __( 'Early Renewal', 'woocommerce-subscriptions' ), 'title' => __( 'Update all subscription payment methods', 'woocommerce-subscriptions' ),
'description' => __( 'Customers can now renew their subscriptions before the scheduled next payment date. Why not use this to email your customers a coupon a month before their annual subscription renewals to get access to that revenue sooner?', 'woocommerce-subscriptions' ), 'description' => __( "Customers will now have the option to update all their subscriptions when they are changing one of their subscription's payment methods - provided the payment gateway supports it.", 'woocommerce-subscriptions' ),
), ),
); );
@@ -109,7 +108,7 @@ class WCS_Upgrade_Notice_Manager {
$notice->set_actions( array( $notice->set_actions( array(
array( array(
'name' => __( 'Learn More', 'woocommerce-subscriptions' ), 'name' => __( 'Learn More', 'woocommerce-subscriptions' ),
'url' => 'https://docs.woocommerce.com/document/subscriptions/version-2-3/', 'url' => 'https://docs.woocommerce.com/document/subscriptions/version-2-5/',
), ),
) ); ) );

View File

@@ -6,6 +6,7 @@
* @category Admin * @category Admin
* @package WooCommerce Subscriptions/Admin/Upgrades * @package WooCommerce Subscriptions/Admin/Upgrades
* @version 2.4.0 * @version 2.4.0
* @deprecated 2.5.0
*/ */
if ( ! defined( 'ABSPATH' ) ) { if ( ! defined( 'ABSPATH' ) ) {
@@ -21,6 +22,8 @@ class WCS_Upgrade_Subscription_Post_Author extends WCS_Background_Upgrader {
* @since 2.4.0 * @since 2.4.0
*/ */
public function __construct( WC_Logger $logger ) { public function __construct( WC_Logger $logger ) {
wcs_deprecated_function( __METHOD__, '2.5.0' );
$this->scheduled_hook = 'wcs_upgrade_subscription_post_author'; $this->scheduled_hook = 'wcs_upgrade_subscription_post_author';
$this->log_handle = 'wcs-upgrade-subscription-post-author'; $this->log_handle = 'wcs-upgrade-subscription-post-author';
$this->logger = $logger; $this->logger = $logger;

View File

@@ -40,7 +40,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php else : ?> <?php else : ?>
<p><?php esc_html_e( 'The update process may take a little while, so please be patient.', 'woocommerce-subscriptions' ); ?></p> <p><?php esc_html_e( 'The update process may take a little while, so please be patient.', 'woocommerce-subscriptions' ); ?></p>
<?php endif; ?> <?php endif; ?>
<p><?php esc_html_e( 'Customers and other non-administrative users can browse and purchase from your store without interuption while the update is in progress.', 'woocommerce-subscriptions' ); ?></p> <p><?php esc_html_e( 'Customers and other non-administrative users can browse and purchase from your store without interruption while the update is in progress.', 'woocommerce-subscriptions' ); ?></p>
<form id="subscriptions-upgrade" method="get" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>"> <form id="subscriptions-upgrade" method="get" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>">
<input type="submit" class="button" value="<?php echo esc_attr_x( 'Update Database', 'text on submit button', 'woocommerce-subscriptions' ); ?>"> <input type="submit" class="button" value="<?php echo esc_attr_x( 'Update Database', 'text on submit button', 'woocommerce-subscriptions' ); ?>">
</form> </form>
@@ -50,7 +50,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<p><?php esc_html_e( 'This page will display the results of the process as each batch of subscriptions is updated.', 'woocommerce-subscriptions' ); ?></p> <p><?php esc_html_e( 'This page will display the results of the process as each batch of subscriptions is updated.', 'woocommerce-subscriptions' ); ?></p>
<p><?php esc_html_e( 'Please keep this page open until the update process completes. No need to refresh or restart the process.', 'woocommerce-subscriptions' ); ?></p> <p><?php esc_html_e( 'Please keep this page open until the update process completes. No need to refresh or restart the process.', 'woocommerce-subscriptions' ); ?></p>
<?php if ( $estimated_duration > 20 ) : ?> <?php if ( $estimated_duration > 20 ) : ?>
<p><?php esc_html_e( 'Remember, although the update process may take a while, customers and other non-administrative users can browse and purchase from your store without interuption while the update is in progress.', 'woocommerce-subscriptions' ); ?></p> <p><?php esc_html_e( 'Remember, although the update process may take a while, customers and other non-administrative users can browse and purchase from your store without interruption while the update is in progress.', 'woocommerce-subscriptions' ); ?></p>
<?php endif; ?> <?php endif; ?>
<ol> <ol>
</ol> </ol>

View File

@@ -192,7 +192,7 @@ function wcs_cart_totals_shipping_method_price_label( $method, $cart ) {
$price_label .= _x( 'Free', 'shipping method price', 'woocommerce-subscriptions' ); $price_label .= _x( 'Free', 'shipping method price', 'woocommerce-subscriptions' );
} }
return $price_label; return apply_filters( 'wcs_cart_totals_shipping_method_price_label', $price_label, $method, $cart );
} }
/** /**
@@ -305,7 +305,7 @@ function wcs_cart_price_string( $recurring_amount, $cart ) {
'subscription_interval' => wcs_cart_pluck( $cart, 'subscription_period_interval' ), 'subscription_interval' => wcs_cart_pluck( $cart, 'subscription_period_interval' ),
'subscription_period' => wcs_cart_pluck( $cart, 'subscription_period', '' ), 'subscription_period' => wcs_cart_pluck( $cart, 'subscription_period', '' ),
'subscription_length' => wcs_cart_pluck( $cart, 'subscription_length' ), 'subscription_length' => wcs_cart_pluck( $cart, 'subscription_length' ),
) ) ); ), $cart ) );
} }
/** /**

View File

@@ -57,36 +57,33 @@ function wcs_help_tip( $tip, $allow_html = false ) {
* @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access. * @param WC_Order|WC_Product|WC_Subscription $object The object whose property we want to access.
* @param string $property The property name. * @param string $property The property name.
* @param string $single Whether to return just the first piece of meta data with the given property key, or all meta data. * @param string $single Whether to return just the first piece of meta data with the given property key, or all meta data.
* @param mixed $default (optional) The value to return if no value is found - defaults to single -> null, multiple -> array() * @param mixed $default (optional) The value to return if no value is found - defaults to single -> null, multiple -> array().
*
* @since 2.2.0 * @since 2.2.0
* @return mixed * @return mixed
*/ */
function wcs_get_objects_property( $object, $property, $single = 'single', $default = null ) { function wcs_get_objects_property( $object, $property, $single = 'single', $default = null ) {
$prefixed_key = wcs_maybe_prefix_key( $property ); $prefixed_key = wcs_maybe_prefix_key( $property );
$value = ! is_null( $default ) ? $default : ( ( 'single' === $single ) ? null : array() );
$property_function_map = array(
'order_version' => 'version',
'order_currency' => 'currency',
'order_date' => 'date_created',
'date' => 'date_created',
'cart_discount' => 'total_discount',
);
$value = ! is_null( $default ) ? $default : ( ( 'single' == $single ) ? null : array() ); if ( isset( $property_function_map[ $property ] ) ) {
$property = $property_function_map[ $property ];
}
switch ( $property ) { switch ( $property ) {
case 'name' : // the replacement for post_title added in 3.0
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
$value = $object->post->post_title;
} else { // WC 3.0+
$value = $object->get_name();
}
break;
case 'post' : case 'post' :
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
$value = $object->post;
} else { // WC 3.0+
// In order to keep backwards compatibility it's required to use the parent data for variations. // In order to keep backwards compatibility it's required to use the parent data for variations.
if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) { if ( method_exists( $object, 'is_type' ) && $object->is_type( 'variation' ) ) {
$value = get_post( $object->get_parent_id() ); $value = get_post( wcs_get_objects_property( $object, 'parent_id' ) );
} else { } else {
$value = get_post( $object->get_id() ); $value = get_post( wcs_get_objects_property( $object, 'id' ) );
}
} }
break; break;
@@ -94,113 +91,32 @@ function wcs_get_objects_property( $object, $property, $single = 'single', $defa
$value = wcs_get_objects_property( $object, 'post' )->post_status; $value = wcs_get_objects_property( $object, 'post' )->post_status;
break; break;
case 'parent_id' :
if ( method_exists( $object, 'get_parent_id' ) ) { // WC 3.0+ or an instance of WC_Product_Subscription_Variation_Legacy with WC < 3.0
$value = $object->get_parent_id();
} else { // WC 2.1-2.6
$value = $object->get_parent();
}
break;
case 'variation_data' : case 'variation_data' :
if ( function_exists( 'wc_get_product_variation_attributes' ) ) { // WC 3.0+ $value = wc_get_product_variation_attributes( wcs_get_objects_property( $object, 'id' ) );
$value = wc_get_product_variation_attributes( $object->get_id() );
} else {
$value = $object->$property;
}
break;
case 'downloads' :
if ( method_exists( $object, 'get_downloads' ) ) { // WC 3.0+
$value = $object->get_downloads();
} else {
$value = $object->get_files();
}
break;
case 'order_version' :
case 'version' :
if ( method_exists( $object, 'get_version' ) ) { // WC 3.0+
$value = $object->get_version();
} else { // WC 2.1-2.6
$value = $object->order_version;
}
break;
case 'order_currency' :
case 'currency' :
if ( method_exists( $object, 'get_currency' ) ) { // WC 3.0+
$value = $object->get_currency();
} else { // WC 2.1-2.6
$value = $object->get_order_currency();
}
break;
// Always return a PHP DateTime object in site timezone (or null), the same thing the WC_Order::get_date_created() method returns in WC 3.0+ to make it easier to migrate away from WC < 3.0
case 'date_created' :
case 'order_date' :
case 'date' :
if ( method_exists( $object, 'get_date_created' ) ) { // WC 3.0+
$value = $object->get_date_created();
} else {
// Base the value off tht GMT value when possible and then set the DateTime's timezone based on the current site's timezone to avoid incorrect values when the timezone has changed
if ( '0000-00-00 00:00:00' != $object->post->post_date_gmt ) {
$value = new WC_DateTime( $object->post->post_date_gmt, new DateTimeZone( 'UTC' ) );
$value->setTimezone( new DateTimeZone( wc_timezone_string() ) );
} else {
$value = new WC_DateTime( $object->post->post_date, new DateTimeZone( wc_timezone_string() ) );
}
}
break;
// Always return a PHP DateTime object in site timezone (or null), the same thing the getter returns in WC 3.0+ to make it easier to migrate away from WC < 3.0
case 'date_paid' :
if ( method_exists( $object, 'get_date_paid' ) ) { // WC 3.0+
$value = $object->get_date_paid();
} else {
if ( ! empty( $object->paid_date ) ) {
// Because the paid_date post meta value was set in the site timezone at the time it was set, this won't always be correct, but is the best we can do with WC < 3.0
$value = new WC_DateTime( $object->paid_date, new DateTimeZone( wc_timezone_string() ) );
} else {
$value = null;
}
}
break;
case 'cart_discount' :
if ( method_exists( $object, 'get_total_discount' ) ) { // WC 3.0+
$value = $object->get_total_discount();
} else { // WC 2.1-2.6
$value = $object->cart_discount;
}
break; break;
default : default :
$function_name = 'get_' . $property; $function_name = 'get_' . $property;
if ( is_callable( array( $object, $function_name ) ) ) { if ( is_callable( array( $object, $function_name ) ) ) {
$value = $object->$function_name(); $value = $object->$function_name();
} else { } else {
// If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that.
// If we don't have a method for this specific property, but we are using WC 3.0, it may be set as meta data on the object so check if we can use that
if ( method_exists( $object, 'get_meta' ) ) {
if ( $object->meta_exists( $prefixed_key ) ) { if ( $object->meta_exists( $prefixed_key ) ) {
if ( 'single' === $single ) { if ( 'single' === $single ) {
$value = $object->get_meta( $prefixed_key, true ); $value = $object->get_meta( $prefixed_key, true );
} else { } else {
// WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available // WC_Data::get_meta() returns an array of stdClass objects with id, key & value properties when meta is available.
$value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' ); $value = wp_list_pluck( $object->get_meta( $prefixed_key, false ), 'value' );
} }
} } elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0.
} elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0
$value = $object->$property; $value = $object->$property;
} elseif ( strtolower( $property ) !== 'id' && metadata_exists( 'post', wcs_get_objects_property( $object, 'id' ), $prefixed_key ) ) { } elseif ( strtolower( $property ) !== 'id' && metadata_exists( 'post', wcs_get_objects_property( $object, 'id' ), $prefixed_key ) ) {
// If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did // If we couldn't find a property or function, fallback to using post meta as that's what many __get() methods in WC < 3.0 did.
if ( 'single' === $single ) { if ( 'single' === $single ) {
$value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, true ); $value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, true );
} else { } else {
// Get all the meta values // Get all the meta values.
$value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, false ); $value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, false );
} }
} }
@@ -519,3 +435,48 @@ function wcs_set_coupon_property( &$coupon, $property, $value ) {
} }
} }
} }
/**
* Generate an order/subscription key.
*
* This is a compatibility wrapper for @see wc_generate_order_key() which was introduced in WC 3.5.4.
*
* @return string $order_key.
* @since 2.5.0
*/
function wcs_generate_order_key() {
if ( function_exists( 'wc_generate_order_key' ) ) {
$order_key = wc_generate_order_key();
} else {
$order_key = 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . wp_generate_password( 13, false ) );
}
return $order_key;
}
/**
* Update a single option for a WC_Settings_API object.
*
* This is a compatibility wrapper for @see WC_Settings_API::update_option() which was introduced in WC 3.4.0.
*
* @param WC_Settings_API $settings_api The object to update the option for.
* @param string $key Option key.
* @param mixed $value Value to set.
* @since 2.5.1
*/
function wcs_update_settings_option( $settings_api, $key, $value ) {
// WooCommerce 3.4+
if ( is_callable( array( $settings_api, 'update_option' ) ) ) {
$settings_api->update_option( $key, $value );
} else {
if ( empty( $settings_api->settings ) ) {
$settings_api->init_settings();
}
$settings_api->settings[ $key ] = $value;
return update_option( $settings_api->get_option_key(), apply_filters( 'woocommerce_settings_api_sanitized_fields_' . $settings_api->id, $settings_api->settings ), 'yes' );
}
}

Some files were not shown because too many files have changed in this diff Show More