This commit is contained in:
Prospress Inc
2017-06-09 16:36:26 +02:00
committed by Remco Tolsma
parent 5e536ec717
commit b99eba2f79
51 changed files with 1413 additions and 642 deletions

View File

@@ -192,6 +192,11 @@ a.close-subscriptions-search {
margin: 0;
}
.subscription_pricing ._subscription_price_field input[type=text],
.subscription_pricing ._subscription_trial_length_field input[type=text] {
padding: 4px;
}
#woocommerce-product-data .wc-metaboxes-wrapper .wc-metabox table td p._subscription_trial_period_field select {
margin-left: 5px;
}
@@ -228,6 +233,14 @@ a.close-subscriptions-search {
height: 31px;
}
.variable_subscription_trial .form-row input[type=text],
.variable_subscription_pricing .form-row input[type=text],
.variable_subscription_trial .form-row select,
.variable_subscription_pricing .form-row select {
margin: 2px 0 0;
padding: 6px;
}
/* Variation Pricing Fields in WooCommerce 2.3+ */
.variable_subscription_pricing_2_3 .wc_input_subscription_price,
.variable_subscription_pricing_2_3 .wc_input_subscription_period_interval {

View File

@@ -356,6 +356,16 @@ jQuery(document).ready(function($){
$( '#_subscription_one_time_shipping' ).prop( 'disabled', is_synced_or_has_trial );
},
showHideSubscriptionsPanels: function() {
var tab = $( 'div.panel-wrap' ).find( 'ul.wc-tabs li' ).eq( 0 ).find( 'a' );
var panel = tab.attr( 'href' );
var visible = $( panel ).children( '.options_group' ).filter( function() {
return 'none' != $( this ).css( 'display' );
});
if ( 0 != visible.length ) {
tab.click().parent().show();
}
},
});
$('.options_group.pricing ._sale_price_field .description').prepend('<span id="sale-price-period" style="display: none;"></span>');
@@ -363,7 +373,6 @@ jQuery(document).ready(function($){
// Move the subscription pricing section to the same location as the normal pricing section
$('.options_group.subscription_pricing').not('.variable_subscription_pricing .options_group.subscription_pricing').insertBefore($('.options_group.pricing:first'));
$('.show_if_subscription.clear').insertAfter($('.options_group.subscription_pricing'));
$( '.show_if_variable' ).addClass( 'show_if_variable-subscription' );
// Move the subscription variation pricing section to a better location in the DOM on load
if($('#variable_product_options .variable_subscription_pricing').length > 0) {
@@ -385,6 +394,7 @@ jQuery(document).ready(function($){
$.setTrialPeriods();
$.showHideSyncOptions();
$.disableEnableOneTimeShipping();
$.showHideSubscriptionsPanels();
}
// Update subscription ranges when subscription period or interval is changed
@@ -404,6 +414,7 @@ jQuery(document).ready(function($){
$.showHideSubscriptionMeta();
$.showHideVariableSubscriptionMeta();
$.showHideSyncOptions();
$.showHideSubscriptionsPanels();
});
$('input#_downloadable, input#_virtual').change(function(){

View File

@@ -1,9 +1,63 @@
*** WooCommerce Subscriptions Changelog ***
2017.05.26 - version 2.2.7
* Tweak: Integrate with My Account > Payment Methods actions to make sure when a customer deletes a payment method, subscriptions using that payment method are automatically updated to use a different token. Or if there are no other payment methods on the customer's account, a customer can't delete the payment method used for automatic payments. PR#1866
* Tweak: Do not display 'Free' on free shipping methods to improve compatibility with the approached used in WooCommerce 2.6 and newer. PR#1766
* Tweak: Call 'wcs_get_retry_rule_raw' even if no rule is defined so that it can be used to add additional rules beyond default rule set. PR#2138
* Tweak: Do not get products for non product post IDs to save performance overhead. PR#2034
* Fix: WooCommerce 3.0: Repair pending cancelled subscriptions that have not got a scheduled action or been correctly transitioned to cancelled as part of the 2.2.7 upgrade process. PR#2129
* Fix: WooCommerce 3.0: Fixes infinite loops when using a coupon for manual renewal via cart by storing coupon properties rather than full coupon objects in the session. PR#2116
* Fix: WooCommerce 3.0: Fix updating a subscription address after paying manual renewal that uses a different address to avoid addresses being incorrectly deleted and not udpated. PR#2145
* Fix: Register subscription report scripts on top level Reports admin screens for uses without store manage capability, but with capability to view reports. PR#2153
* Fix: Make sure login is required when purchasing subscriptions products and "Registration on checkout" is disabled. PR#1822
* Fix: Use more than 'ipn_track_id' for different PayPal IPN messages now that PayPal has suddently begun to use the same 'ipn_track_id' for different IPN messages. PR#2090
* Fix: Trigger order.created webhook when creating renewal orders by Subscriptions with WC < 3.0. PR#1793
* Fix: Lookup one time shipping setting value on the parent product on variations rather than on the variations to make sure we find the correct value. PR#2125
* Fix: Do not cancel subscriptions that are not using PayPal as the payment method when a PayPal Billing Agreement is cancelled (including subscriptions using manual renewal). PR#2127
* Fix: Allow single payment subscriptions to be cancelled by customer if that single payment is still in future (because there was a free trial). PR#2060
* Fix: Avoid errors by making sure deleted subscriptions are not returned in set of switched and users subscriptions by respective functions for getting those subscriptions. PR#2075 and PR#2131
* Fix: Change Edit Subscriptions admin screen "This order is no longer editable" text to avoid confusion. PR#2128
* Fix: Do not ignore variations with empty prices during min/max calculations to improve compatibility with 3rd party plugins, like Name Your Price. PR#2120
* Fix: Check status can be changed on payment failure to avoid throwing exceptions/errors when the status can't be changed, which is also almost always signifies that the status doesn't need to be changed to reflect the failure. PR#2140
* Fix: Make it is possible to change payment method to PayPal Standard for subscriptions imported or manually added (meaning they do not have a parent order). PR#1956
* Fix: Restore previous v3 customer/{id}/subscriptions response structure to REST API. PR#2150
* Fix: Add support for filtering subscriptions by customer ID in v3 legacy REST API. PR#2144
* Fix: Show General tab for variable subscription product edit page on page load. PR#2044
* Fix: Delete the retry date after subscription status changed. PR#2143
2017.05.05 - version 2.2.6
* Fix: WooCommerce 3.0: Guard against infinite loops caused by 3rd party code calling order methods with a post object by making sure we have an order object, not just an object. PR#2100
* Fix: WooCommerce 3.0: Improve compatibility with 3rd party code that adds custom address fields by making sure we check address method is callable before calling it, and falling back to meta data if that method doesn't exist. PR#2102
* Fix: WooCommerce 3.0: Set correct line item meta names when paying for a renewal or resubscribe order by not relying on WooCommerce 3.0's array access implementation. PR#2105
* Fix: WooCommerce 3.0: Assorted issues with dates not being scheduled and/or not being set at the correct time when running WooCommerce 3.0 by making sure we only save date properties after updating or deleting dates rather than also saving status transitions. PR#2091
* Fix: WooCommerce 3.0: Match WooCommerce 3.0 approach to adding variation attributes to cart/order line item name by making sure we use the line item name rather than product title to alter renewal cart item names. PR#2109
* Fix: WooCommerce 3.0: Match WooCommerce 3.0 filters for sign-up fee prices. Fixes compatibility with sign-up fees and Memberships. PR#2110
* Fix: Make sure a variable product is purchasable even if the variations tab wasn't loaded before saving the product, and make sure we sync the variable product's prices even if the variation's tabe wasn't loaded. PR#2111
* Fix: Make sure subscription is seen as having a zero total when customer completes the Change Payment Method flow to avoid charging anything at the time of changing payment method. PR#2082
* Fix: Fix resubscribing to products for 1 payment cuased by inconsistent variable type being returned by WC_Subscription::get_billing_interval(). PR#2098
* Fix: "Trying to get property of non-object" notice by checking order_type property exists before trying to access it. PR#2099
* Fix: Correct subscription variation field alignmenton WooCommerce > Edit Product screen. PR#2104
* Tweak: Improve upgrade logging by recording version at upgrade and not deleting logs after 6 weeks so we can trace time and day of upgrades in support and better diagnose issues. PR#2070
* Tweak: Adds more flexibility to next payment date recalculation on activation of a subscription with new 'woocommerce_subscription_activation_next_payment_date_threshold' hook. PR#1860
* Tweak: Improve when variations are saved by moving them to save on the 'woocommerce_save_product_variation' hook. PR#2111
* Tweak: Update Action Scheduler to v1.5.3. PR#2113
2017.04.21 - version 2.2.5
* Fix: WooCommerce 3.0: Only call get_id() if product is an object and has not been deleted. PR#2071
* Fix: WooCommerce 3.0: Do not attempt to access WC()->payment_gateways->payment_gateways() when reading the subscription object and setting the payment method, because there is no guarantee WC() will be setup yet (and we only need to do it when the method is called not when it is being instantiated. PR#2084
* Fix: WooCommerce 3.0: Do not set payment method title in set payment method function when instantiating subscription (i.e. prior to it being read). PR#2084
* Fix: WooCommerce 3.0: Calculate the next payment date using either last order paid date or the last order date, whichever is the later, to maintain backward compatiblity with versions prior to WooCommerce 3.0 support. PR#2087
* Fix: WooCommerce 3.0: Replace use of deprecated WooCommerce function removed in WC 3.0. PR#2088
* Fix: PHP 7.1 notices with products with empty signup fees and other properties after WooCommerce 3.0 changes for getting meta on products. PR#2064
* Fix: Allow dynamically setting properties on a subscription via WC_Subscription::__set() for backwards compatibility. PR#2079
* Fix: Do not validate admin payment method changes which will not result in a change to allow for changing meta on a payment method that is not active. PR#2085
* Tweak: Add wc_input_price class to subscription product price inputs to make sure they use decimal separator and other store settings for validation. PR#2068
2017.04.12 - version 2.2.4
* Fix: WooCommerce 3.0: Correctly handle switching between grouped products in WooCommerce 3.0 where a product can have more than one parent grouped product. PR#2063
* Fix: Do not incorrectly create pending renewal orders and suspend PayPal Standard Subscriptions at the time their payment is due. PayPal controls the billing schedule for these subscriptions. PR#2069
* Tweak: Add 'woocommerce_subscriptions_order_type_dropdown' filter. PR#2065
* Tweak: Include only Subscriptions strings in POT file.
2017.04.07 - version 2.2.3
* Fix: WooCommerce 3.0: Improve backward compatibility of get_date( 'start' ) calls for subscriptions without a date created set by WooCommerce 3.0 (which happens when manually adding a new subscriptions). PR#2047

View File

@@ -84,7 +84,7 @@ class WC_Subscriptions_Admin {
// Save variable subscription meta
add_action( 'woocommerce_process_product_meta_variable-subscription', __CLASS__ . '::process_product_meta_variable_subscription' );
add_action( 'woocommerce_ajax_save_product_variations', __CLASS__ . '::process_product_meta_variable_subscription' );
add_action( 'woocommerce_save_product_variation', __CLASS__ . '::save_product_variation', 20, 2 );
add_action( 'woocommerce_subscription_pre_update_status', __CLASS__ . '::check_customer_is_set', 10, 3 );
@@ -122,6 +122,11 @@ class WC_Subscriptions_Admin {
add_filter( 'woocommerce_get_formatted_order_total', __CLASS__ . '::maybe_remove_formatted_order_total_filter', 0, 2 );
add_action( 'woocommerce_payment_gateways_settings', __CLASS__ . '::add_recurring_payment_gateway_information', 10 , 1 );
// Change text for when order items cannot be edited
add_action( 'woocommerce_admin_order_totals_after_refunded', __CLASS__ . '::maybe_attach_gettext_callback', 10, 1 );
// Unhook gettext callback to prevent extra call impact
add_action( 'woocommerce_order_item_add_action_buttons', __CLASS__ . '::maybe_unattach_gettext_callback', 10, 1 );
}
/**
@@ -205,7 +210,7 @@ class WC_Subscriptions_Admin {
?><p class="form-field _subscription_price_fields _subscription_price_field">
<label for="_subscription_price"><?php printf( esc_html__( 'Subscription price (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) ); ?></label>
<span class="wrap">
<input type="text" id="_subscription_price" name="_subscription_price" class="wc_input_subscription_price" placeholder="<?php echo esc_attr_x( 'e.g. 5.90', 'example price', 'woocommerce-subscriptions' ); ?>" step="any" min="0" value="<?php echo esc_attr( $chosen_price ); ?>" />
<input type="text" id="_subscription_price" name="_subscription_price" class="wc_input_price wc_input_subscription_price" placeholder="<?php echo esc_attr_x( 'e.g. 5.90', 'example price', 'woocommerce-subscriptions' ); ?>" step="any" min="0" value="<?php echo esc_attr( $chosen_price ); ?>" />
<label for="_subscription_period_interval" class="wcs_hidden_label"><?php esc_html_e( 'Subscription interval', 'woocommerce-subscriptions' ); ?></label>
<select id="_subscription_period_interval" name="_subscription_period_interval" class="wc_input_subscription_period_interval">
<?php foreach ( wcs_get_subscription_period_interval_strings() as $value => $label ) { ?>
@@ -236,7 +241,7 @@ class WC_Subscriptions_Admin {
// Sign-up Fee
woocommerce_wp_text_input( array(
'id' => '_subscription_sign_up_fee',
'class' => 'wc_input_subscription_intial_price short',
'class' => 'wc_input_subscription_intial_price wc_input_price short',
// translators: %s is a currency symbol / code
'label' => sprintf( __( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ),
'placeholder' => _x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ),
@@ -576,50 +581,55 @@ class WC_Subscriptions_Admin {
*/
public static function process_product_meta_variable_subscription( $post_id ) {
if ( ! WC_Subscriptions_Product::is_subscription( $post_id ) || empty( $_POST['_wcsnonce_save_variations'] ) || ! wp_verify_nonce( $_POST['_wcsnonce_save_variations'], 'wcs_subscription_variations' ) ) {
if ( ! WC_Subscriptions_Product::is_subscription( $post_id ) || empty( $_POST['_wcsnonce'] ) || ! wp_verify_nonce( $_POST['_wcsnonce'], 'wcs_subscription_meta' ) ) {
return;
}
// Make sure WooCommerce calculates correct prices
$_POST['variable_regular_price'] = isset( $_POST['variable_subscription_price'] ) ? $_POST['variable_subscription_price'] : 0;
if ( ! isset( $_REQUEST['variable_post_id'] ) ) {
// Sync the min variation price
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
$variable_subscription = wc_get_product( $post_id );
$variable_subscription->variable_product_sync();
} else {
WC_Product_Variable::sync( $post_id );
}
}
/**
* Save meta info for subscription variations
*
* @param int $variation_id
* @param int $i
* return void
* @since 2.0
*/
public static function save_product_variation( $variation_id, $index ) {
if ( ! WC_Subscriptions_Product::is_subscription( $variation_id ) || empty( $_POST['_wcsnonce_save_variations'] ) || ! wp_verify_nonce( $_POST['_wcsnonce_save_variations'], 'wcs_subscription_variations' ) ) {
return;
}
$variable_post_ids = $_POST['variable_post_id'];
$max_loop = max( array_keys( $variable_post_ids ) );
// Save each variations details
for ( $i = 0; $i <= $max_loop; $i ++ ) {
if ( ! isset( $variable_post_ids[ $i ] ) ) {
continue;
}
$variation_id = absint( $variable_post_ids[ $i ] );
if ( isset( $_POST['variable_subscription_price'] ) && is_array( $_POST['variable_subscription_price'] ) ) {
$subscription_price = wc_format_decimal( $_POST['variable_subscription_price'][ $i ] );
if ( isset( $_POST['variable_subscription_price'][ $index ] ) ) {
$subscription_price = wc_format_decimal( $_POST['variable_subscription_price'][ $index ] );
update_post_meta( $variation_id, '_subscription_price', $subscription_price );
update_post_meta( $variation_id, '_regular_price', $subscription_price );
}
// Make sure trial period is within allowable range
$subscription_ranges = wcs_get_subscription_ranges();
$max_trial_length = count( $subscription_ranges[ $_POST['variable_subscription_trial_period'][ $index ] ] ) - 1;
$max_trial_length = count( $subscription_ranges[ $_POST['variable_subscription_trial_period'][ $i ] ] ) - 1;
$_POST['variable_subscription_trial_length'][ $index ] = absint( $_POST['variable_subscription_trial_length'][ $index ] );
$_POST['variable_subscription_trial_length'][ $i ] = absint( $_POST['variable_subscription_trial_length'][ $i ] );
if ( $_POST['variable_subscription_trial_length'][ $i ] > $max_trial_length ) {
$_POST['variable_subscription_trial_length'][ $i ] = $max_trial_length;
if ( $_POST['variable_subscription_trial_length'][ $index ] > $max_trial_length ) {
$_POST['variable_subscription_trial_length'][ $index ] = $max_trial_length;
}
// Work around a WPML bug which means 'variable_subscription_trial_period' is not set when using "Edit Product" as the product translation interface
if ( $_POST['variable_subscription_trial_length'][ $i ] < 0 ) {
$_POST['variable_subscription_trial_length'][ $i ] = 0;
if ( $_POST['variable_subscription_trial_length'][ $index ] < 0 ) {
$_POST['variable_subscription_trial_length'][ $index ] = 0;
}
$subscription_fields = array(
@@ -632,21 +642,12 @@ class WC_Subscriptions_Admin {
);
foreach ( $subscription_fields as $field_name ) {
if ( isset( $_POST[ 'variable' . $field_name ][ $i ] ) ) {
update_post_meta( $variation_id, $field_name, wc_clean( $_POST[ 'variable' . $field_name ][ $i ] ) );
if ( isset( $_POST[ 'variable' . $field_name ][ $index ] ) ) {
update_post_meta( $variation_id, $field_name, wc_clean( $_POST[ 'variable' . $field_name ][ $index ] ) );
}
}
}
// Now that all the variation's meta is saved, sync the min variation price
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
$variable_subscription = wc_get_product( $post_id );
$variable_subscription->variable_product_sync();
} else {
WC_Product_Variable::sync( $post_id );
}
}
/**
* Make sure when saving a subscription via the admin to activate it, it has a valid customer set on it.
*
@@ -1518,6 +1519,57 @@ class WC_Subscriptions_Admin {
return $formatted_total;
}
/**
* Only attach the gettext callback when on admin shop subscription screen
*
* @since 2.2.7
*/
public static function maybe_attach_gettext_callback() {
$screen = get_current_screen();
if ( is_object( $screen ) && 'shop_subscription' == $screen->id ) {
add_filter( 'gettext', __CLASS__ . '::change_order_item_editable_text', 10, 3 );
}
}
/**
* Only unattach the gettext callback when it was attached
*
* @since 2.2.7
*/
public static function maybe_unattach_gettext_callback() {
$screen = get_current_screen();
if ( is_object( $screen ) && 'shop_subscription' == $screen->id ) {
remove_filter( 'gettext', __CLASS__ . '::change_order_item_editable_text', 10, 3 );
}
}
/**
* When subscription items not editable (such as due to the payment gateway not supporting modifications),
* change the text to explain why
*
* @since 2.2.7
*/
public static function change_order_item_editable_text( $translated_text, $text, $domain ) {
switch ( $text ) {
case 'This order is no longer editable.':
$translated_text = __( 'Subscription items can no longer be edited.', 'woocommerce-subscriptions' );
break;
case 'To edit this order change the status back to "Pending"':
$translated_text = __( 'This subscription is no longer editable because the payment gateway does not allow modification of recurring amounts.', 'woocommerce-subscriptions' );
break;
}
return $translated_text;
}
/**
* Add recurring payment gateway information after the Settings->Checkout->Payment Gateways table.
* This includes links to find additional gateways, information about manual renewals

View File

@@ -132,7 +132,7 @@ class WCS_Admin_Reports {
$wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce-subscriptions' ) );
// Reports Subscriptions Pages
if ( in_array( $screen->id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'dashboard' ) ) ) && isset( $_GET['tab'] ) && 'subscriptions' == $_GET['tab'] ) {
if ( in_array( $screen->id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) && isset( $_GET['tab'] ) && 'subscriptions' == $_GET['tab'] ) {
wp_enqueue_script( 'wcs-reports', plugin_dir_url( WC_Subscriptions::$plugin_file ) . 'assets/js/admin/reports.js', array( 'jquery', 'jquery-ui-datepicker', 'wc-reports', 'accounting' ), WC_Subscriptions::$version );

View File

@@ -121,9 +121,13 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {
$function_name = 'get_billing_' . $key;
if ( $subscription->$function_name() ) {
echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( make_clickable( esc_html( $subscription->$function_name() ) ) ) . '</p>';
if ( is_callable( array( $subscription, $function_name ) ) ) {
$field_value = $subscription->$function_name( 'edit' );
} else {
$field_value = $subscription->get_meta( '_billing_' . $key );
}
echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( make_clickable( esc_html( $field_value ) ) ) . '</p>';
}
echo '<p' . ( ( '' != $subscription->get_payment_method() ) ? ' class="' . esc_attr( $subscription->get_payment_method() ) . '"' : '' ) . '><strong>' . esc_html__( 'Payment Method', 'woocommerce-subscriptions' ) . ':</strong>' . wp_kses_post( nl2br( $subscription->get_payment_method_to_display() ) );
@@ -187,11 +191,15 @@ class WCS_Meta_Box_Subscription_Data extends WC_Meta_Box_Order_Data {
continue;
}
$field_name = 'shipping_' . $key;
$function_name = 'get_shipping_' . $key;
if ( ! empty( $subscription->$field_name ) ) {
echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( make_clickable( esc_html( $subscription->$field_name ) ) ) . '</p>';
if ( is_callable( array( $subscription, $function_name ) ) ) {
$field_value = $subscription->$function_name( 'edit' );
} else {
$field_value = $subscription->get_meta( '_shipping_' . $key );
}
echo '<p><strong>' . esc_html( $field['label'] ) . ':</strong> ' . wp_kses_post( make_clickable( esc_html( $field_value ) ) ) . '</p>';
}
}

View File

@@ -88,7 +88,7 @@ class WC_REST_Subscriptions_Controller extends WC_REST_Orders_V1_Controller {
$date_type_key = ( 'start' === $date_type ) ? 'date_created' : $date_type;
$date = $subscription->get_date( $date_type_key );
$response->data[ $date_type . '_date'] = ( ! empty( $date ) ) ? wc_rest_prepare_date_response( $date ) : '';
$response->data[ $date_type . '_date' ] = ( ! empty( $date ) ) ? wc_rest_prepare_date_response( $date ) : '';
}
}

View File

@@ -66,16 +66,22 @@ class WC_API_Subscriptions_Customers extends WC_API_Customers {
if ( is_wp_error( $id ) ) {
return $id;
}
$subscription_ids = array();
$customer_subscriptions = $subscription_ids = array();
$filter['customer_id'] = $id;
$subscriptions = WC()->api->WC_API_Subscriptions->get_subscriptions( $fields, $filter, null, -1 );
if ( ! empty( $subscriptions['subscriptions'] ) && is_array( $subscriptions['subscriptions'] ) ) {
foreach ( $subscriptions['subscriptions'] as $subscription ) {
if ( isset( $subscription['billing_schedule']['interval'] ) ) { // make sure the interval is not a string to fully support backwards compat.
$subscription['billing_schedule']['interval'] = intval( $subscription['billing_schedule']['interval'] );
}
$customer_subscriptions[] = array( 'subscription' => $subscription );
$subscription_ids[] = $subscription['id'];
}
}
return array( 'customer_subscriptions' => apply_filters( 'wc_subscriptions_api_customer_subscriptions', $subscriptions, $id, $fields, $subscription_ids, $this->server ) );
return array( 'customer_subscriptions' => apply_filters( 'wc_subscriptions_api_customer_subscriptions', $customer_subscriptions, $id, $fields, $subscription_ids, $this->server ) );
}
}

View File

@@ -694,7 +694,16 @@ class WC_API_Subscriptions extends WC_API_Orders {
$query_args['post_status'] = $statuses;
unset( $args['status'] );
}
if ( ! empty( $args['customer_id'] ) ) {
$query_args['meta_query'] = array(
array(
'key' => '_customer_user',
'value' => absint( $args['customer_id'] ),
'compare' => '=',
),
);
}
$query_args = $this->merge_query_args( $query_args, $args );

View File

@@ -176,6 +176,9 @@ class WC_Subscription extends WC_Order {
if ( ! WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
wcs_doing_it_wrong( $key, sprintf( 'Subscription properties should not be set directly as WooCommerce 3.0 no longer supports direct property access. Use %s instead.', $function ), '2.2.0' );
}
} else {
$this->$key = $value;
}
}
@@ -455,8 +458,8 @@ class WC_Subscription extends WC_Order {
// Recalculate and set next payment date
$stored_next_payment = $this->get_time( 'next_payment' );
// Make sure the next payment date is more than 2 hours in the future
if ( $stored_next_payment < ( gmdate( 'U' ) + 2 * HOUR_IN_SECONDS ) ) { // also accounts for a $stored_next_payment of 0, meaning it's not set
// Make sure the next payment date is more than 2 hours in the future by default
if ( $stored_next_payment < ( gmdate( 'U' ) + apply_filters( 'woocommerce_subscription_activation_next_payment_date_threshold', 2 * HOUR_IN_SECONDS, $stored_next_payment, $old_status, $this ) ) ) { // also accounts for a $stored_next_payment of 0, meaning it's not set
$calculated_next_payment = $this->calculate_date( 'next_payment' );
@@ -465,6 +468,9 @@ class WC_Subscription extends WC_Order {
} elseif ( $stored_next_payment < gmdate( 'U' ) ) { // delete the stored date if it's in the past as we're not updating it (the calculated next payment date is 0 or none)
$this->delete_date( 'next_payment' );
}
} else {
// 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 );
}
// 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() );
@@ -846,7 +852,7 @@ class WC_Subscription extends WC_Order {
* @param int $value
*/
public function set_billing_interval( $value ) {
$this->set_prop( 'billing_interval', absint( $value ) );
$this->set_prop( 'billing_interval', (string) absint( $value ) );
}
/**
@@ -1251,8 +1257,8 @@ class WC_Subscription extends WC_Order {
}
if ( $is_updated && true === $this->object_read ) {
$this->save_dates();
do_action( 'woocommerce_subscription_date_updated', $this, $date_type, $datetime );
$this->save();
}
}
}
@@ -1287,8 +1293,8 @@ class WC_Subscription extends WC_Order {
$this->set_date_prop( $date_type, 0 );
if ( true === $this->object_read ) {
$this->save_dates();
do_action( 'woocommerce_subscription_date_deleted', $this, $date_type );
$this->save();
}
}
@@ -1392,7 +1398,7 @@ class WC_Subscription extends WC_Order {
$start_time = $this->get_time( 'date_created' );
$next_payment_time = $this->get_time( 'next_payment' );
$trial_end_time = $this->get_time( 'trial_end' );
$last_payment_time = $this->get_time( 'last_order_date_created' );
$last_payment_time = max( $this->get_time( 'last_order_date_created' ), $this->get_time( 'last_order_date_paid' ) );
$end_time = $this->get_time( 'end' );
// If the subscription has a free trial period, and we're still in the free trial period, the next payment is due at the end of the free trial
@@ -1435,6 +1441,24 @@ class WC_Subscription extends WC_Order {
return $next_payment_date;
}
/**
* Complete a partial save, saving subscription date changes to the database.
*
* Sometimes it's necessary to only save changes to date properties, for example, when you
* don't want status transitions to be triggered by a full object @see $this->save().
*
* @since 2.2.6
*/
public function save_dates() {
if ( $this->data_store && $this->get_id() ) {
$saved_dates = $this->data_store->save_dates( $this );
// Apply the saved date changes
$this->data = array_replace_recursive( $this->data, $saved_dates );
$this->changes = array_diff_key( $this->changes, $saved_dates );
}
}
/** Formatted Totals Methods *******************************************************/
/**
@@ -1548,7 +1572,7 @@ class WC_Subscription extends WC_Order {
}
// Remove discounts
$subtotal = $subtotal - $this->get_cart_discount();
$subtotal = $subtotal - $this->get_total_discount();
$subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) );
}
@@ -1693,8 +1717,10 @@ class WC_Subscription extends WC_Order {
// Allow a short circuit for plugins & payment gateways to force max failed payments exceeded
if ( 'cancelled' == $new_status || apply_filters( 'woocommerce_subscription_max_failed_payments_exceeded', false, $this ) ) {
if ( $this->can_be_updated_to( 'cancelled' ) ) {
$this->update_status( 'cancelled', __( 'Subscription Cancelled: maximum number of failed payments reached.', 'woocommerce-subscriptions' ) );
} else {
}
} elseif ( $this->can_be_updated_to( $new_status ) ) {
$this->update_status( $new_status );
}
@@ -1937,6 +1963,9 @@ class WC_Subscription extends WC_Order {
if ( $this->get_payment_method() !== $payment_method_id ) {
// We shouldn't set the requires manual renewal prop or try to get the payment gateway while the object is being read. That prop should be set by reading it from the DB not based on settings or the payment gateway
if ( $this->object_read ) {
// Set the payment gateway ID depending on whether we have a string or WC_Payment_Gateway or string key
if ( is_a( $payment_method, 'WC_Payment_Gateway' ) ) {
$payment_gateway = $payment_method;
@@ -1945,8 +1974,6 @@ class WC_Subscription extends WC_Order {
$payment_gateway = isset( $payment_gateways[ $payment_method_id ] ) ? $payment_gateways[ $payment_method_id ] : null;
}
// We shouldn't set the requires manual renewal prop while the object is being read. That prop should be set by reading it from the DB not based on settings or the payment gateway
if ( $this->object_read ) {
if ( 'yes' == get_option( WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no' ) ) {
$this->set_requires_manual_renewal( true );
} elseif ( is_null( $payment_gateway ) || false == $payment_gateway->supports( 'subscriptions' ) ) {
@@ -1954,10 +1981,11 @@ class WC_Subscription extends WC_Order {
} else {
$this->set_requires_manual_renewal( false );
}
$this->set_prop( 'payment_method_title', is_null( $payment_gateway ) ? '' : $payment_gateway->get_title() );
}
$this->set_prop( 'payment_method', $payment_method_id );
$this->set_prop( 'payment_method_title', is_null( $payment_gateway ) ? '' : $payment_gateway->get_title() );
}
}
}

View File

@@ -154,10 +154,14 @@ class WC_Subscriptions_Addresses {
$subscription = wcs_get_subscription( absint( $_GET['subscription'] ) );
foreach ( array_keys( $address ) as $key ) {
$function_name = 'get_' . $key;
if ( is_callable( array( $subscription, $function_name ) ) ) {
$address[ $key ]['value'] = $subscription->$function_name();
}
}
}
return $address;
}

View File

@@ -665,7 +665,6 @@ class WC_Subscriptions_Cart {
if ( WC_Subscriptions_Product::is_subscription( $product ) && ! wcs_cart_contains_renewal() ) {
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
$product_price_filter = 'woocommerce_get_price';
} else {

View File

@@ -81,7 +81,7 @@ class WC_Subscriptions_Change_Payment_Gateway {
} else {
// If we're changing the payment method, we want to make sure a number of totals return $0 (to prevent payments being processed now)
add_filter( 'woocommerce_order_get_total', __CLASS__ . '::maybe_zero_total', 11, 2 );
add_filter( 'woocommerce_subscription_get_total', __CLASS__ . '::maybe_zero_total', 11, 2 );
}
}

View File

@@ -11,8 +11,6 @@
*/
class WC_Subscriptions_Checkout {
private static $signup_option_changed = false;
private static $guest_checkout_option_changed = false;
/**
@@ -389,25 +387,16 @@ class WC_Subscriptions_Checkout {
if ( WC_Subscriptions_Cart::cart_contains_subscription() && ! is_user_logged_in() ) {
// Make sure users can sign up
if ( false === $checkout->enable_signup ) {
$checkout->enable_signup = true;
self::$signup_option_changed = true;
}
// Make sure users are required to register an account
if ( true === $checkout->enable_guest_checkout ) {
$checkout->enable_guest_checkout = false;
self::$guest_checkout_option_changed = true;
if ( ! is_user_logged_in() ) {
$checkout->must_create_account = true;
}
}
}
}
/**
* Make sure account fields display the required "*" when they are required.
*
@@ -440,10 +429,6 @@ class WC_Subscriptions_Checkout {
*/
public static function restore_checkout_registration_settings( $checkout = '' ) {
if ( self::$signup_option_changed ) {
$checkout->enable_signup = false;
}
if ( self::$guest_checkout_option_changed ) {
$checkout->enable_guest_checkout = true;
if ( ! is_user_logged_in() ) { // Also changed must_create_account

View File

@@ -75,7 +75,7 @@ class WC_Subscriptions_Coupon {
*/
public static function get_discount_amount( $discount, $discounting_amount, $cart_item, $single, $coupon ) {
$coupon_type = wcs_get_coupon_property( $coupon, 'type' );
$coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' );
// Only deal with subscriptions coupon types
if ( ! in_array( $coupon_type, array( 'recurring_fee', 'recurring_percent', 'sign_up_fee', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) {
@@ -163,12 +163,12 @@ class WC_Subscriptions_Coupon {
$discounting_amount = 0;
}
$discount_amount = min( wcs_get_coupon_property( $coupon, 'amount' ), $discounting_amount );
$discount_amount = min( wcs_get_coupon_property( $coupon, 'coupon_amount' ), $discounting_amount );
$discount_amount = $single ? $discount_amount : $discount_amount * $cart_item_qty;
} elseif ( $apply_recurring_percent_coupon ) {
$discount_amount = ( $discounting_amount / 100 ) * wcs_get_coupon_property( $coupon, 'amount' );
$discount_amount = ( $discounting_amount / 100 ) * wcs_get_coupon_property( $coupon, 'coupon_amount' );
} elseif ( $apply_initial_percent_coupon ) {
@@ -177,7 +177,7 @@ class WC_Subscriptions_Coupon {
$discounting_amount = 0;
}
$discount_amount = ( $discounting_amount / 100 ) * wcs_get_coupon_property( $coupon, 'amount' );
$discount_amount = ( $discounting_amount / 100 ) * wcs_get_coupon_property( $coupon, 'coupon_amount' );
} elseif ( $apply_renewal_cart_coupon ) {
@@ -189,7 +189,7 @@ class WC_Subscriptions_Coupon {
*/
$discount_percent = ( $discounting_amount * $cart_item['quantity'] ) / self::get_renewal_subtotal( wcs_get_coupon_property( $coupon, 'code' ) );
$discount_amount = ( wcs_get_coupon_property( $coupon, 'amount' ) * $discount_percent ) / $cart_item_qty;
$discount_amount = ( wcs_get_coupon_property( $coupon, 'coupon_amount' ) * $discount_percent ) / $cart_item_qty;
}
// Round - consistent with WC approach
@@ -222,7 +222,7 @@ class WC_Subscriptions_Coupon {
foreach ( WC()->cart->applied_coupons as $code ) {
$coupon = new WC_Coupon( $code );
$cart_coupon_type = wcs_get_coupon_property( $coupon, 'type' );
$cart_coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' );
if ( 'any' == $coupon_type || $coupon_type == $cart_coupon_type || ( 'core' == $coupon_type && in_array( $cart_coupon_type, $core_coupons ) ) ) {
$contains_discount = true;
@@ -246,7 +246,7 @@ class WC_Subscriptions_Coupon {
}
self::$coupon_error = '';
$coupon_type = wcs_get_coupon_property( $coupon, 'type' );
$coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' );
// ignore non-subscription coupons
if ( ! in_array( $coupon_type, array( 'recurring_fee', 'sign_up_fee', 'recurring_percent', 'sign_up_fee_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ) ) ) {
@@ -379,7 +379,7 @@ class WC_Subscriptions_Coupon {
foreach ( $applied_coupons as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code );
$coupon_type = wcs_get_coupon_property( $coupon, 'type' );
$coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' );
if ( in_array( $coupon_type, array( 'recurring_fee', 'recurring_percent' ) ) ) { // always apply coupons to their specific calculation case
if ( 'recurring_total' == $calculation_type ) {
@@ -443,9 +443,9 @@ class WC_Subscriptions_Coupon {
foreach ( $renewal_coupons as $subscription_id => $coupons ) {
foreach ( $coupons as $coupon ) {
foreach ( $coupons as $coupon_code => $coupon_properties ) {
if ( wcs_get_coupon_property( $coupon, 'code' ) == $code ) {
if ( $coupon_code == $code ) {
if ( $subscription = wcs_get_subscription( $subscription_id ) ) {
$subtotal = $subscription->get_subtotal();
@@ -530,8 +530,8 @@ class WC_Subscriptions_Coupon {
foreach ( $cart->applied_coupons as $coupon_code ) {
$coupon = new WC_Coupon( $coupon_code );
$coupon_type = wcs_get_coupon_property( $coupon, 'type' );
$coupon_amount = wcs_get_coupon_property( $coupon, 'amount' );
$coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' );
$coupon_amount = wcs_get_coupon_property( $coupon, 'coupon_amount' );
// Pre 2.5 is_valid_for_product() does not use wc_get_product_coupon_types()
if ( WC_Subscriptions::is_woocommerce_pre( '2.5' ) ) {

View File

@@ -108,15 +108,20 @@ class WC_Subscriptions_Product {
*/
public static function is_subscription( $product ) {
$is_subscription = false;
$is_subscription = $product_id = false;
$product = self::maybe_get_product_instance( $product );
if ( is_object( $product ) && $product->is_type( array( 'subscription', 'subscription_variation', 'variable-subscription' ) ) ) {
if ( is_object( $product ) ) {
$product_id = $product->get_id();
if ( $product->is_type( array( 'subscription', 'subscription_variation', 'variable-subscription' ) ) ) {
$is_subscription = true;
}
}
return apply_filters( 'woocommerce_is_subscription', $is_subscription, $product->get_id(), $product );
return apply_filters( 'woocommerce_is_subscription', $is_subscription, $product_id, $product );
}
/**
@@ -438,11 +443,11 @@ class WC_Subscriptions_Product {
* Returns the subscription interval for a product, if it's a subscription.
*
* @param mixed $product A WC_Product object or product ID
* @return string A string representation of the period, either Day, Week, Month or Year, or an empty string if product is not a subscription.
* @return int An integer representing the subscription interval, or 1 if the product is not a subscription or there is no interval
* @since 1.0
*/
public static function get_interval( $product ) {
return apply_filters( 'woocommerce_subscriptions_product_period_interval', self::get_meta_data( $product, 'subscription_period_interval', 0 ), self::maybe_get_product_instance( $product ) );
return apply_filters( 'woocommerce_subscriptions_product_period_interval', self::get_meta_data( $product, 'subscription_period_interval', 1, 'use_default_value' ), self::maybe_get_product_instance( $product ) );
}
/**
@@ -453,7 +458,7 @@ class WC_Subscriptions_Product {
* @since 1.0
*/
public static function get_length( $product ) {
return apply_filters( 'woocommerce_subscriptions_product_length', self::get_meta_data( $product, 'subscription_length', 0 ), self::maybe_get_product_instance( $product ) );
return apply_filters( 'woocommerce_subscriptions_product_length', self::get_meta_data( $product, 'subscription_length', 0, 'use_default_value' ), self::maybe_get_product_instance( $product ) );
}
/**
@@ -464,7 +469,7 @@ class WC_Subscriptions_Product {
* @since 1.0
*/
public static function get_trial_length( $product ) {
return apply_filters( 'woocommerce_subscriptions_product_trial_length', self::get_meta_data( $product, 'subscription_trial_length', 0 ), self::maybe_get_product_instance( $product ) );
return apply_filters( 'woocommerce_subscriptions_product_trial_length', self::get_meta_data( $product, 'subscription_trial_length', 0, 'use_default_value' ), self::maybe_get_product_instance( $product ) );
}
/**
@@ -486,7 +491,7 @@ class WC_Subscriptions_Product {
* @since 1.0
*/
public static function get_sign_up_fee( $product ) {
return apply_filters( 'woocommerce_subscriptions_product_sign_up_fee', self::get_meta_data( $product, 'subscription_sign_up_fee', 0 ), self::maybe_get_product_instance( $product ) );
return apply_filters( 'woocommerce_subscriptions_product_sign_up_fee', self::get_meta_data( $product, 'subscription_sign_up_fee', 0, 'use_default_value' ), self::maybe_get_product_instance( $product ) );
}
/**
@@ -673,7 +678,7 @@ class WC_Subscriptions_Product {
public static function user_can_not_delete_subscription( $allcaps, $caps, $args ) {
global $wpdb;
if ( isset( $args[0] ) && in_array( $args[0], array( 'delete_post', 'delete_product' ) ) && isset( $args[2] ) && ( ! isset( $_GET['action'] ) || 'untrash' != $_GET['action'] ) ) {
if ( isset( $args[0] ) && in_array( $args[0], array( 'delete_post', 'delete_product' ) ) && isset( $args[2] ) && ( ! isset( $_GET['action'] ) || 'untrash' != $_GET['action'] ) && 0 === strpos( get_post_type( $args[2] ), 'product' ) ) {
$user_id = $args[2];
$post_id = $args[2];
@@ -748,7 +753,11 @@ class WC_Subscriptions_Product {
* @since 2.2.0
*/
public static function needs_one_time_shipping( $product ) {
return apply_filters( 'woocommerce_subscriptions_product_needs_one_time_shipping', 'yes' === self::get_meta_data( $product, 'subscription_one_time_shipping', 'no' ), self::maybe_get_product_instance( $product ) );
$product = self::maybe_get_product_instance( $product );
if ( $product && $product->is_type( 'variation' ) && is_callable( array( $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 );
}
/**
@@ -960,10 +969,12 @@ class WC_Subscriptions_Product {
*
* @param mixed $product A WC_Product object or product ID
* @param string $meta_key The string key for the meta data
* @return float The value of the sign-up fee, or 0 if the product is not a subscription or the subscription has no sign-up fee
* @param mixed $default_value The value to return if the meta doesn't exist or isn't set
* @param string $empty_handling (optional) How empty values should be handled -- can be 'use_default_value' or 'allow_empty'. Defaults to 'allow_empty' returning the empty value.
* @return mixed
* @since 2.2.0
*/
public static function get_meta_data( $product, $meta_key, $default_value ) {
public static function get_meta_data( $product, $meta_key, $default_value, $empty_handling = 'allow_empty' ) {
$product = self::maybe_get_product_instance( $product );
@@ -984,6 +995,10 @@ class WC_Subscriptions_Product {
}
}
if ( 'use_default_value' === $empty_handling && empty( $meta_value ) ) {
$meta_value = $default_value;
}
return $meta_value;
}

View File

@@ -93,10 +93,10 @@ class WC_Subscriptions_Renewal_Order {
);
wp_update_post( $update_post_data );
update_post_meta( $order_id, '_paid_date', current_time( 'mysql', true ) );
update_post_meta( $order_id, '_paid_date', current_time( 'mysql' ) );
} else {
// In WC 3.0, only the paid date prop represents the paid date, the post date isn't used anymore, also the paid date is stored and referenced as a timestamp in site timezone, not a MySQL string
$order->set_date_paid( current_time( 'timestamp', 0 ) );
// In WC 3.0, only the paid date prop represents the paid date, the post date isn't used anymore, also the paid date is stored and referenced as a MySQL date string in site timezone and a GMT timestamp
$order->set_date_paid( current_time( 'timestamp', 1 ) );
$order->save();
}
}
@@ -192,7 +192,7 @@ class WC_Subscriptions_Renewal_Order {
foreach ( $order_items as $order_item_id => $item ) {
if ( is_callable( array( $item, 'delete_meta_data' ) ) ) { // WC 3.0+
foreach( $switched_order_item_keys as $switch_meta_key => $value ) {
foreach ( $switched_order_item_keys as $switch_meta_key => $value ) {
$item->delete_meta_data( $switch_meta_key );
}
} else { // WC 2.6

View File

@@ -970,17 +970,16 @@ class WC_Subscriptions_Switcher {
// Select the subscriptions which had item/s switched to this subscription by its parent order
if ( ! empty( $post->post_parent ) ) {
$switched_ids = wcs_get_objects_property( wc_get_order( $post->post_parent ), 'subscription_switch', 'multiple' );
$switched_subscriptions = wcs_get_subscriptions_for_switch_order( $post->post_parent );
}
// On the Edit Order screen, show any subscriptions with items switched by this order
} else {
$switched_ids = wcs_get_objects_property( wc_get_order( $post->ID ), 'subscription_switch', 'multiple' );
$switched_subscriptions = wcs_get_subscriptions_for_switch_order( $post->ID );
}
if ( is_array( $switched_ids ) ) {
foreach ( $switched_ids as $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( is_array( $switched_subscriptions ) ) {
foreach ( $switched_subscriptions as $subscription_id => $subscription ) {
wcs_set_objects_property( $subscription, 'relationship', __( 'Switched Subscription', 'woocommerce-subscriptions' ), 'set_prop_only' );
$orders[ $subscription_id ] = $subscription;
}

View File

@@ -64,7 +64,7 @@ class WC_Subscriptions_Synchroniser {
// Save sync options when a variable subscription product is saved
add_action( 'woocommerce_process_product_meta_variable-subscription', __CLASS__ . '::process_product_meta_variable_subscription' );
add_action( 'woocommerce_ajax_save_product_variations', __CLASS__ . '::process_product_meta_variable_subscription' );
add_action( 'woocommerce_save_product_variation', __CLASS__ . '::save_product_variation', 20, 2 );
// Make sure the expiration dates are calculated from the synced start date
add_filter( 'woocommerce_subscriptions_product_trial_expiration_date', __CLASS__ . '::recalculate_product_trial_expiration_date', 10, 2 );
@@ -331,43 +331,36 @@ class WC_Subscriptions_Synchroniser {
return;
}
$variable_post_ids = $_POST['variable_post_id'];
$max_loop = max( array_keys( $variable_post_ids ) );
// Make sure the parent product doesn't have a sync value (in case it was once a simple subscription)
update_post_meta( $post_id, self::$post_meta_key, 0 );
}
/**
* Save sync options when a variable subscription product is saved
*
* @since 1.5
*/
public static function save_product_variation( $variation_id, $index ) {
if ( empty( $_POST['_wcsnonce_save_variations'] ) || ! wp_verify_nonce( $_POST['_wcsnonce_save_variations'], 'wcs_subscription_variations' ) || ! isset( $_POST['variable_post_id'] ) || ! is_array( $_POST['variable_post_id'] ) ) {
return;
}
$day_field = 'variable' . self::$post_meta_key_day;
$month_field = 'variable' . self::$post_meta_key_month;
// Save each variations details
for ( $i = 0; $i <= $max_loop; $i ++ ) {
if ( 'year' == $_POST['variable_subscription_period'][ $index ] ) { // save the day & month for the date rather than just the day
if ( ! isset( $variable_post_ids[ $i ] ) ) {
continue;
}
$variation_id = absint( $variable_post_ids[ $i ] );
if ( 'year' == $_POST['variable_subscription_period'][ $i ] ) { // save the day & month for the date rather than just the day
$_POST[ 'variable' . self::$post_meta_key ][ $i ] = array(
'day' => isset( $_POST[ $day_field ][ $i ] ) ? $_POST[ $day_field ][ $i ] : 0,
'month' => isset( $_POST[ $month_field ][ $i ] ) ? $_POST[ $month_field ][ $i ] : 0,
$_POST[ 'variable' . self::$post_meta_key ][ $index ] = array(
'day' => isset( $_POST[ $day_field ][ $index ] ) ? $_POST[ $day_field ][ $index ] : 0,
'month' => isset( $_POST[ $month_field ][ $index ] ) ? $_POST[ $month_field ][ $index ] : 0,
);
} else {
if ( ! isset( $_POST[ 'variable' . self::$post_meta_key ][ $i ] ) ) {
$_POST[ 'variable' . self::$post_meta_key ][ $i ] = 0;
}
} elseif ( ! isset( $_POST[ 'variable' . self::$post_meta_key ][ $index ] ) ) {
$_POST[ 'variable' . self::$post_meta_key ][ $index ] = 0;
}
if ( isset( $_POST[ 'variable' . self::$post_meta_key ][ $i ] ) ) {
update_post_meta( $variation_id, self::$post_meta_key, $_POST[ 'variable' . self::$post_meta_key ][ $i ] );
}
}
update_post_meta( $variation_id, self::$post_meta_key, $_POST[ 'variable' . self::$post_meta_key ][ $index ] );
}
/**

View File

@@ -35,9 +35,6 @@ class WCS_Cart_Renewal {
// Remove order action buttons from the My Account page
add_filter( 'woocommerce_my_account_my_orders_actions', array( &$this, 'filter_my_account_my_orders_actions' ), 10, 2 );
// Update customer's address on the subscription if it is changed during renewal
add_filter( 'woocommerce_checkout_update_customer_data', array( &$this, 'maybe_update_subscription_customer_data' ), 10, 2 );
// When a failed renewal order is paid for via checkout, make sure WC_Checkout::create_order() preserves its "failed" status until it is paid
add_filter( 'woocommerce_default_order_status', array( &$this, 'maybe_preserve_order_status' ) );
@@ -65,6 +62,8 @@ class WCS_Cart_Renewal {
// When a renewal order's line items are being updated, update the line item IDs stored in cart data.
add_action( 'woocommerce_add_order_item_meta', array( &$this, 'update_line_item_cart_data' ), 10, 3 );
add_filter( 'woocommerce_checkout_update_customer_data', array( &$this, 'maybe_update_subscription_customer_data' ), 10, 2 );
} else {
// For order items created as part of a renewal, keep a record of the cart item key so that we can match it later once the order item has been saved and has an ID
@@ -75,6 +74,9 @@ class WCS_Cart_Renewal {
// Don't display cart item key meta stored above on the Edit Order screen
add_action( 'woocommerce_hidden_order_itemmeta', array( &$this, 'hidden_order_itemmeta' ), 10 );
// Update customer's address on the subscription if it is changed during renewal
add_filter( 'woocommerce_checkout_update_user_meta', array( &$this, 'maybe_update_subscription_address_data' ), 10, 2 );
}
}
@@ -189,9 +191,10 @@ class WCS_Cart_Renewal {
}
/**
* Set up cart item meta data for a to complete a subscription renewal via the cart.
* Set up cart item meta data to complete a subscription renewal via the cart.
*
* @since 2.0
* @since 2.2.0
* @version 2.2.6
*/
protected function setup_cart( $subscription, $cart_item_data ) {
@@ -199,13 +202,18 @@ class WCS_Cart_Renewal {
$success = true;
foreach ( $subscription->get_items() as $item_id => $line_item ) {
// Load all product info including variation data
$product_id = (int) apply_filters( 'woocommerce_add_to_cart_product_id', $line_item['product_id'] );
$quantity = (int) $line_item['qty'];
$variation_id = (int) $line_item['variation_id'];
$variations = array();
$item_data = array();
// Load all product info including variation data
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
$product_id = (int) $line_item['product_id'];
$quantity = (int) $line_item['qty'];
$variation_id = (int) $line_item['variation_id'];
$item_name = $line_item['name'];
foreach ( $line_item['item_meta'] as $meta_name => $meta_value ) {
if ( taxonomy_is_product_attribute( $meta_name ) ) {
$variations[ $meta_name ] = $meta_value[0];
@@ -213,8 +221,24 @@ class WCS_Cart_Renewal {
$variations[ $meta_name ] = $meta_value[0];
}
}
} else {
$product = wc_get_product( $line_item['product_id'] );
$product_id = $line_item->get_product_id();
$quantity = $line_item->get_quantity();
$variation_id = $line_item->get_variation_id();
$item_name = $line_item->get_name();
foreach ( $line_item->get_meta_data() as $meta ) {
if ( taxonomy_is_product_attribute( $meta->key ) ) {
$variations[ $meta->key ] = $meta->value;
} elseif ( meta_is_product_attribute( $meta->key, $meta->value, $product_id ) ) {
$variations[ $meta->key ] = $meta->value;
}
}
}
$product_id = apply_filters( 'woocommerce_add_to_cart_product_id', $product_id );
$product = wc_get_product( $product_id );
// The notice displayed when a subscription product has been deleted and the custoemr attempts to manually renew or make a renewal payment for a failed recurring payment for that product/subscription
// translators: placeholder is an item name
@@ -223,16 +247,16 @@ class WCS_Cart_Renewal {
// Display error message for deleted products
if ( false === $product ) {
wc_add_notice( sprintf( $product_deleted_error_message, $line_item['name'] ), 'error' );
wc_add_notice( sprintf( $product_deleted_error_message, $item_name ), 'error' );
// Make sure we don't actually need the variation ID (if the product was a variation, it will have a variation ID; however, if the product has changed from a simple subscription to a variable subscription, there will be no variation_id)
} elseif ( $product->is_type( array( 'variable-subscription' ) ) && ! empty( $line_item['variation_id'] ) ) {
} elseif ( $product->is_type( array( 'variable-subscription' ) ) && ! empty( $variation_id ) ) {
$variation = wc_get_product( $variation_id );
// Display error message for deleted product variations
if ( false === $variation ) {
wc_add_notice( sprintf( $product_deleted_error_message, $line_item['name'] ), 'error' );
wc_add_notice( sprintf( $product_deleted_error_message, $item_name ), 'error' );
}
}
@@ -279,7 +303,7 @@ class WCS_Cart_Renewal {
foreach ( $coupon_items as $coupon_item ) {
$coupon = new WC_Coupon( $coupon_item['name'] );
$coupon_type = wcs_get_coupon_property( $coupon, 'type' );
$coupon_type = wcs_get_coupon_property( $coupon, 'discount_type' );
$coupon_code = '';
// If the coupon still exists we can use the existing/available coupon properties
@@ -290,9 +314,9 @@ class WCS_Cart_Renewal {
// Set the coupon type to be a renewal equivalent for correct validation and calculations
if ( 'recurring_percent' == $coupon_type ) {
wcs_set_coupon_property( $coupon, 'type', 'renewal_percent' );
wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_percent' );
} elseif ( 'recurring_fee' == $coupon_type ) {
wcs_set_coupon_property( $coupon, 'type', 'renewal_fee' );
wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_fee' );
}
// Adjust coupon code to reflect that it is being applied to a renewal
@@ -301,8 +325,8 @@ class WCS_Cart_Renewal {
} else {
// If the coupon doesn't exist we can only really apply the discount amount we know about - so we'll apply a cart style pseudo coupon and then set the amount
wcs_set_coupon_property( $coupon, 'type', 'renewal_cart' );
wcs_set_coupon_property( $coupon, 'amount', $coupon_item['item_meta']['discount_amount']['0'] );
wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_cart' );
wcs_set_coupon_property( $coupon, 'coupon_amount', $coupon_item['item_meta']['discount_amount']['0'] );
// Adjust coupon code to reflect that it is being applied to a renewal
$coupon_code = wcs_get_coupon_property( $coupon, 'code' );
@@ -331,9 +355,9 @@ class WCS_Cart_Renewal {
$coupon = new WC_Coupon( 'discount_renewal' );
// Apply our cart style pseudo coupon and the set the amount
wcs_set_coupon_property( $coupon, 'type', 'renewal_cart' );
wcs_set_coupon_property( $coupon, 'discount_type', 'renewal_cart' );
wcs_set_coupon_property( $coupon, 'amount', $subscription_discount );
wcs_set_coupon_property( $coupon, 'coupon_amount', $subscription_discount );
// Set renewal order products as the product ids on the coupon
if ( ! WC_Subscriptions::is_woocommerce_pre( '2.5' ) ) {
@@ -432,7 +456,8 @@ class WCS_Cart_Renewal {
wcs_set_objects_property( $_product, 'subscription_sign_up_fee', 0, 'set_prop_only' );
// Allow plugins to add additional strings to the product name for renewals
wcs_set_objects_property( $_product, 'name', apply_filters( 'woocommerce_subscriptions_renewal_product_title', $_product->get_title(), $_product ), 'set_prop_only' );
$line_item_name = is_callable( $item_to_renew, 'get_name' ) ? $item_to_renew->get_name() : $item_to_renew['name'];
wcs_set_objects_property( $_product, 'name', apply_filters( 'woocommerce_subscriptions_renewal_product_title', $line_item_name, $_product ), 'set_prop_only' );
// Make sure the same quantity is renewed
$cart_item_session_data['quantity'] = $item_to_renew['qty'];
@@ -744,58 +769,22 @@ class WCS_Cart_Renewal {
foreach ( $renewal_coupons as $subscription_id => $coupons ) {
foreach ( $coupons as $coupon ) {
foreach ( $coupons as $coupon_code => $coupon_properties ) {
// Tweak the coupon data for renewal coupons
if ( wcs_get_coupon_property( $coupon, 'code' ) == $code ) {
if ( $coupon_code == $code ) {
$expiry_date_property = WC_Subscriptions::is_woocommerce_pre( '3.0' ) ? 'expiry_date' : 'date_expires';
$data = array(
// Some coupon properties are overridden specifically for renewals
$renewal_coupon_overrides = array(
'id' => true,
'discount_type' => wcs_get_coupon_property( $coupon, 'type' ),
'amount' => wcs_get_coupon_property( $coupon, 'amount' ),
'individual_use' => ( $individual_use = wcs_get_coupon_property( $coupon, 'individual_use' ) ) ? $individual_use : false,
'product_ids' => ( $product_ids = wcs_get_coupon_property( $coupon, 'product_ids' ) ) ? $product_ids : array(),
'excluded_product_ids' => ( $excluded_product_ids = wcs_get_coupon_property( $coupon, 'exclude_product_ids' ) ) ? $excluded_product_ids : array(),
'usage_limit' => '',
'usage_count' => '',
'date_expires' => '',
'free_shipping' => ( $free_shipping = wcs_get_coupon_property( $coupon, 'free_shipping' ) ) ? $free_shipping : false,
'product_categories' => ( $product_categories = wcs_get_coupon_property( $coupon, 'product_categories' ) ) ? $product_categories : array(),
'excluded_product_categories' => ( $excluded_product_categories = wcs_get_coupon_property( $coupon, 'exclude_product_categories' ) ) ? $excluded_product_categories : array(),
'exclude_sale_items' => ( $exclude_sale_items = wcs_get_coupon_property( $coupon, 'exclude_sale_items' ) ) ? $exclude_sale_items : false,
'minimum_amount' => ( $minimum_amount = wcs_get_coupon_property( $coupon, 'minimum_amount' ) ) ? $minimum_amount : '',
'maximum_amount' => ( $maximum_amount = wcs_get_coupon_property( $coupon, 'maximum_amount' ) ) ? $maximum_amount : '',
'customer_email' => ( $customer_email = wcs_get_coupon_property( $coupon, 'customer_email' ) ) ? $customer_email : array(),
$expiry_date_property => '',
);
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
// Pre 3.0 we don't need to pass the id.
unset( $data['id'] );
// Some keys have changed between WC 2.6.x and WC 3.0. This array holds those changes in a 2.6 => 3.0 format.
$property_changes = array(
'coupon_amount' => 'amount',
'exclude_product_ids' => 'excluded_product_ids',
'expiry_date' => 'date_expires',
'exclude_product_categories' => 'excluded_product_categories',
'customer_email' => 'email_restrictions',
);
foreach ( $data as $key => $value ) {
// Switch the 3.0 key out for the 2.6 equivalent
if ( in_array( $key, $property_changes ) ) {
$data[ array_search( $key, $property_changes ) ] = $value;
unset( $data[ $key ] );
}
// Some coupon properties have changed from accepting 'no' and 'yes' to true and false args. We need to change them into the correct format
if ( is_bool( $value ) && in_array( $key, array( 'individual_use', 'free_shipping', 'exclude_sale_items' ) ) ) {
$data[ $key ] = ( true == $value ) ? 'yes' : 'no';
}
}
}
$data = array_merge( $coupon_properties, $renewal_coupon_overrides );
break 2;
}
}
}
@@ -835,14 +824,54 @@ class WCS_Cart_Renewal {
*/
protected function store_coupon( $subscription_id, $coupon ) {
if ( ! empty( $subscription_id ) && ! empty( $coupon ) ) {
$renewal_coupons = WC()->session->get( 'wcs_renewal_coupons', array() );
$use_bools = WC_Subscriptions::is_woocommerce_pre( '3.0' ); // Some coupon properties have changed from accepting 'no' and 'yes' to true and false args.
$coupon_properties = array();
$property_defaults = array(
'discount_type' => '',
'amount' => 0,
'individual_use' => ( $use_bools ) ? false : 'no',
'product_ids' => array(),
'excluded_product_ids' => array(),
'free_shipping' => ( $use_bools ) ? false : 'no',
'product_categories' => array(),
'excluded_product_categories' => array(),
'exclude_sale_items' => ( $use_bools ) ? false : 'no',
'minimum_amount' => '',
'maximum_amount' => '',
'email_restrictions' => array(),
);
// Subscriptions may have multiple coupons, store coupons in array
foreach ( $property_defaults as $property => $value ) {
$getter = 'get_' . $property;
if ( is_callable( array( $coupon, $getter ) ) ) {
$value = $coupon->$getter();
} else { // WC < 3.0
// Map the property to its version compatible name ( 3.0+ => WC < 3.0 )
$getter_to_property_map = array(
'amount' => 'coupon_amount',
'excluded_product_ids' => 'exclude_product_ids',
'date_expires' => 'expiry_date',
'excluded_product_categories' => 'exclude_product_categories',
'email_restrictions' => 'customer_email',
);
$property = array_key_exists( $property, $getter_to_property_map ) ? $getter_to_property_map[ $property ] : $property;
if ( property_exists( $coupon, $property ) ) {
$value = $coupon->$property;
}
}
$coupon_properties[ $property ] = $value;
}
// Subscriptions may have multiple coupons, store coupons in an array
if ( array_key_exists( $subscription_id, $renewal_coupons ) ) {
$renewal_coupons[ $subscription_id ][] = $coupon;
$renewal_coupons[ $subscription_id ][ wcs_get_coupon_property( $coupon, 'code' ) ] = $coupon_properties;
} else {
$renewal_coupons[ $subscription_id ] = array( $coupon );
$renewal_coupons[ $subscription_id ] = array( wcs_get_coupon_property( $coupon, 'code' ) => $coupon_properties );
}
WC()->session->set( 'wcs_renewal_coupons', $renewal_coupons );
@@ -861,8 +890,8 @@ class WCS_Cart_Renewal {
// Remove the coupons from the cart
if ( ! empty( $renewal_coupons ) ) {
foreach ( $renewal_coupons as $subscription_id => $coupons ) {
foreach ( $coupons as $coupon ) {
WC()->cart->remove_coupons( wcs_get_coupon_property( $coupon, 'code' ) );
foreach ( $coupons as $coupon_code => $coupon_properties ) {
WC()->cart->remove_coupons( $coupon_code );
}
}
}
@@ -1081,6 +1110,39 @@ class WCS_Cart_Renewal {
return $hidden_meta_keys;
}
/**
* When completing checkout for a subscription renewal, update the subscription's address to match
* the shipping/billing address entered on checkout.
*
* @param int $customer_id
* @param array $checkout_data the posted checkout data
* @since 2.2.7
*/
public function maybe_update_subscription_address_data( $customer_id, $checkout_data ) {
$cart_renewal_item = $this->cart_contains();
if ( false !== $cart_renewal_item ) {
$subscription = wcs_get_subscription( $cart_renewal_item[ $this->cart_item_key ]['subscription_id'] );
$billing_address = $shipping_address = array();
foreach ( array( 'billing', 'shipping' ) as $address_type ) {
$checkout_fields = WC()->checkout()->get_checkout_fields( $address_type );
if ( is_array( $checkout_fields ) ) {
foreach ( array_keys( $checkout_fields ) as $field ) {
if ( isset( $checkout_data[ $field ] ) ) {
$field_name = str_replace( $address_type. '_', '', $field );
${$address_type . '_address'}[ $field_name ] = $checkout_data[ $field ];
}
}
}
}
$subscription->set_address( $billing_address, 'billing' );
$subscription->set_address( $shipping_address, 'shipping' );
}
}
/* Deprecated */
/**

View File

@@ -27,6 +27,19 @@ class WCS_Cart_Switch extends WCS_Cart_Renewal {
add_action( 'template_redirect', array( &$this, 'maybe_setup_cart' ), 99 );
}
/**
* Attach WooCommerce version dependent hooks
*
* @since 2.2.0
*/
public function attach_dependant_hooks() {
parent::attach_dependant_hooks();
// Remove version dependent callbacks which don't apply to switch carts
remove_filter( 'woocommerce_checkout_update_customer_data', array( &$this, 'maybe_update_subscription_customer_data' ), 10 );
remove_filter( 'woocommerce_checkout_update_user_meta', array( &$this, 'maybe_update_subscription_address_data' ), 10 );
}
/**
* Add flag to payment url for failed/ pending switch orders.
*

View File

@@ -109,7 +109,7 @@ class WCS_Change_Payment_Method_Admin {
$valid_payment_methods = self::get_valid_payment_methods( $subscription );
if ( ! isset( $valid_payment_methods[ $payment_method ] ) ) {
if ( ! isset( $valid_payment_methods[ $payment_method ] ) && ! ( isset( $payment_gateways[ $payment_method ] ) && $subscription->get_payment_method() == $payment_gateways[ $payment_method ]->id ) ) {
throw new Exception( __( 'Please choose a valid payment gateway to change to.', 'woocommerce-subscriptions' ) );
}

View File

@@ -0,0 +1,239 @@
<?php
/**
* Manage the process of deleting, adding, assigning default payment tokens associated with automatic subscriptions
*
* @package WooCommerce Subscriptions
* @category Class
* @author Prospress
* @since 2.2.7
*/
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.
*
* @since 2.2.7
*/
public static function init() {
// Only hook class functions if the payment token object exists
if ( class_exists( 'WC_Payment_Token' ) ) {
add_filter( 'woocommerce_payment_methods_list_item', __CLASS__ . '::flag_subscription_payment_token_deletions', 10, 2 );
add_action( 'woocommerce_payment_token_deleted', __CLASS__ . '::maybe_update_subscriptions_payment_meta', 10, 2 );
}
}
/**
* Add additional query args to delete token URLs which are being used for subscription automatic payments.
*
* @param array data about the token including a list of actions which can be triggered by the customer from their my account page
* @param WC_Payment_Token payment token object
* @return array payment token data
* @since 2.2.7
*/
public static function flag_subscription_payment_token_deletions( $payment_token_data, $payment_token ) {
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 ( self::customer_has_alternative_token( $payment_token ) ) {
$delete_subscription_token_args = array(
'delete_subscription_token' => $payment_token->get_id(),
'wcs_nonce' => wp_create_nonce( 'delete_subscription_token_' . $payment_token->get_id() ),
);
$payment_token_data['actions']['delete']['url'] = add_query_arg( $delete_subscription_token_args, $payment_token_data['actions']['delete']['url'] );
} else {
// Cannot delete a token used for active subscriptions where there is no alternative
unset( $payment_token_data['actions']['delete'] );
}
}
}
return $payment_token_data;
}
/**
* Update subscriptions using a deleted token to use a new token. Subscriptions with the
* old token value stored in post meta will be updated using the same meta key to use the
* new token value.
*
* @param int The deleted token id
* @param WC_Payment_Token The deleted token object
* @since 2.2.7
*/
public static function maybe_update_subscriptions_payment_meta( $deleted_token_id, $deleted_token ) {
if ( isset( $_GET['delete_subscription_token'] ) && ! empty( $_GET['wcs_nonce'] ) && wp_verify_nonce( $_GET['wcs_nonce'], 'delete_subscription_token_' . $_GET['delete_subscription_token'] ) ) {
// init payment gateways
WC()->payment_gateways();
$new_token = self::get_customers_alternative_token( $deleted_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' );
wc_add_notice( $notice, 'error' );
return;
}
$subscriptions = self::get_subscriptions_by_token( $deleted_token );
$token_meta_key = '';
$notice_displayed = false;
if ( ! empty( $subscriptions ) ) {
// translators: $1: the token/credit card label, 2$-3$: opening and closing strong and link tags
$notice = sprintf( esc_html__( 'The deleted payment method was used for automatic subscription payments. To avoid failed renewal payments in future the subscriptions using this payment method have been updated to use your %1$s. To change the payment method of individual subscriptions go to your %2$sMy Account > Subscriptions%3$s page.', 'woocommerce-subscriptions' ),
self::get_token_label( $new_token ),
'<a href="' . esc_url( wc_get_account_endpoint_url( get_option( 'woocommerce_myaccount_subscriptions_endpoint', 'subscriptions' ) ) ) . '"><strong>',
'</strong></a>'
);
wc_add_notice( $notice , 'notice' );
foreach ( $subscriptions as $subscription ) {
$subscription = wcs_get_subscription( $subscription );
if ( empty( $subscription ) ) {
continue;
}
// Attempt to find the token meta key if we haven't already found it.
if ( empty( $token_meta_key ) ) {
$payment_method_meta = apply_filters( 'woocommerce_subscription_payment_meta', array(), $subscription );
if ( is_array( $payment_method_meta ) && isset( $payment_method_meta[ $deleted_token->get_gateway_id() ] ) && is_array( $payment_method_meta[ $deleted_token->get_gateway_id() ] ) ) {
foreach ( $payment_method_meta[ $deleted_token->get_gateway_id() ] as $meta_table => $meta ) {
foreach ( $meta as $meta_key => $meta_data ) {
if ( $deleted_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(), $deleted_token->get_token() );
if ( $updated ) {
$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() ) );
do_action( 'woocommerce_subscription_token_changed', $subscription, $new_token, $deleted_token );
}
}
}
}
}
/**
* 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(
'key' => '_customer_user',
'value' => $payment_token->get_user_id(),
'type' => 'numeric',
),
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,
) );
return apply_filters( 'woocommerce_subscriptions_by_payment_token', $user_subscriptions, $payment_token );
}
/**
* Get a WC_Payment_Token label. eg Visa ending in 1234
*
* @param WC_Payment_Token payment token object
* @return string WC_Payment_Token label
* @since 2.2.7
*/
public static function get_token_label( $token ) {
if ( method_exists( $token, 'get_last4' ) && $token->get_last4() ) {
$label = sprintf( __( '%s ending in %s', 'woocommerce-subscriptions' ), esc_html( wc_get_credit_card_type_label( $token->get_card_type() ) ), esc_html( $token->get_last4() ) );
} else {
$label = esc_html( wc_get_credit_card_type_label( $token->get_card_type() ) );
}
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 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;
$has_single_alternative = count( $payment_tokens ) === 2; // if there are 2 tokens in total there is only 1 other alternative
foreach ( $payment_tokens as $payment_token ) {
// if there is a default token which is different we can use it as an alternative.
if ( $payment_token->get_id() !== $token->get_id() && ( $payment_token->is_default() || $has_single_alternative ) ) {
$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;
}
}
WCS_My_Account_Payment_Methods::init();

View File

@@ -145,6 +145,7 @@ class WCS_Retry_Manager {
// If the new status isn't the expected retry subscription status and we aren't in the process of applying a retry rule or retrying payment, cancel the retry
if ( $new_status != $retry_subscription_status && ! $applying_retry_rule && ! $retrying_payment ) {
$last_retry->update_status( 'cancelled' );
$subscription->delete_date( 'payment_retry' );
}
}
}

View File

@@ -26,7 +26,7 @@ class WCS_Webhooks {
*/
public static function init() {
add_filter( 'woocommerce_webhook_topic_hooks', __CLASS__ . '::add_topics', 10, 2 );
add_filter( 'woocommerce_webhook_topic_hooks', __CLASS__ . '::add_topics', 20, 2 );
add_filter( 'woocommerce_webhook_payload', __CLASS__ . '::create_payload', 10, 4 );
@@ -42,6 +42,20 @@ class WCS_Webhooks {
add_filter( 'woocommerce_webhook_topics' , __CLASS__ . '::add_topics_admin_menu', 10, 1 );
add_filter( 'wcs_new_order_created', __CLASS__ . '::add_subscription_created_order_callback', 10, 1 );
}
/**
* Trigger `order.create` every time an order is created by Subscriptions.
*
* @param WC_Order $order WC_Order Object
*/
public static function add_subscription_created_order_callback( $order ) {
do_action( 'wcs_webhook_order_created', wcs_get_objects_property( $order, 'id' ) );
return $order;
}
/**
@@ -52,7 +66,12 @@ class WCS_Webhooks {
*/
public static function add_topics( $topic_hooks, $webhook ) {
if ( 'subscription' == $webhook->get_resource() ) {
switch ( $webhook->get_resource() ) {
case 'order':
$topic_hooks['order.created'][] = 'wcs_webhook_order_created';
break;
case 'subscription':
$topic_hooks = apply_filters( 'woocommerce_subscriptions_webhook_topics', array(
'subscription.created' => array(
'wcs_api_subscription_created',
@@ -74,6 +93,7 @@ class WCS_Webhooks {
'wcs_webhook_subscription_switched',
),
), $webhook );
break;
}
return $topic_hooks;

View File

@@ -259,4 +259,54 @@ class WCS_Subscription_Data_Store_CPT extends WC_Order_Data_Store_CPT implements
return $return;
}
}
/**
* Update subscription dates in the database.
*
* @param WC_Subscription $subscription
* @return array The date properties saved to the database in the format: array( $prop_name => DateTime Object )
* @since 2.2.6
*/
public function save_dates( $subscription ) {
$saved_dates = array();
$changes = $subscription->get_changes();
$date_meta_keys = array(
'_schedule_trial_end',
'_schedule_next_payment',
'_schedule_cancelled',
'_schedule_end',
'_schedule_payment_retry',
);
$date_meta_keys_to_props = array_intersect_key( $this->subscription_meta_keys_to_props, array_flip( $date_meta_keys ) );
// Save the changes to scheduled dates
foreach ( $this->get_props_to_update( $subscription, $date_meta_keys_to_props ) as $meta_key => $prop ) {
update_post_meta( $subscription->get_id(), $meta_key, $subscription->get_date( $prop ) );
$saved_dates[ $prop ] = wcs_get_datetime_from( $subscription->get_time( $prop ) );
}
$post_data = array();
// Save any changes to the created date
if ( isset( $changes['date_created'] ) ) {
$post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $subscription->get_date_created( 'edit' )->getOffsetTimestamp() );
$post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $subscription->get_date_created( 'edit' )->getTimestamp() );
$saved_dates['date_created'] = $subscription->get_date_created();
}
// Save any changes to the modified date
if ( isset( $changes['date_modified'] ) ) {
$post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $subscription->get_date_modified( 'edit' )->getOffsetTimestamp() );
$post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $subscription->get_date_modified( 'edit' )->getTimestamp() );
$saved_dates['date_modified'] = $subscription->get_date_modified();
}
if ( ! empty( $post_data ) ) {
$post_data['ID'] = $subscription->get_id();
wp_update_post( $post_data );
}
return $saved_dates;
}
}

View File

@@ -117,11 +117,14 @@ class WCS_PayPal_Reference_Transaction_IPN_Handler extends WCS_PayPal_Standard_I
$subscription = wcs_get_subscription( $subscription_id );
// Only cancel valid subscriptions using PayPal as the payment method that have not yet been ended
if ( false == $subscription || $subscription->is_manual() || 'paypal' != $subscription->get_payment_method() || $subscription->has_status( wcs_get_subscription_ended_statuses() ) ) {
continue;
}
try {
if ( false !== $subscription && ! $subscription->has_status( wcs_get_subscription_ended_statuses() ) ) {
$subscription->cancel_order( $note );
WC_Gateway_Paypal::log( sprintf( 'Subscription %s Cancelled: %s', $subscription_id, $note ) );
}
} catch ( Exception $e ) {
WC_Gateway_Paypal::log( sprintf( 'Unable to cancel subscription %s: %s', $subscription_id, $e->getMessage() ) );
}

View File

@@ -103,29 +103,42 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
exit;
}
if ( isset( $transaction_details['ipn_track_id'] ) ) {
if ( isset( $transaction_details['txn_id'] ) ) {
// Make sure the IPN request has not already been handled
$handled_ipn_requests = get_post_meta( $subscription->get_id(), '_paypal_ipn_tracking_ids', true );
$handled_transactions = get_post_meta( $subscription->get_id(), '_paypal_ipn_tracking_ids', true );
if ( empty( $handled_ipn_requests ) ) {
$handled_ipn_requests = array();
if ( empty( $handled_transactions ) ) {
$handled_transactions = array();
}
// The 'ipn_track_id' is not a unique ID and is shared between different transaction types, so create a unique ID by prepending the transaction type
$ipn_id = $transaction_details['txn_type'] . '_' . $transaction_details['ipn_track_id'];
// $ipn_transaction_id will be 'txn_id'_'txn_type'_'payment_status'_'ipn_track_id'
$ipn_transaction_id = $transaction_details['txn_id'];
if ( in_array( $ipn_id, $handled_ipn_requests ) ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: IPN ' . $ipn_id . ' message has already been correctly handled.' );
if ( isset( $transaction_details['txn_type'] ) ) {
$ipn_transaction_id .= '_' . $transaction_details['txn_type'];
}
// The same transaction ID is used for different payment statuses, so make sure we handle it only once. See: http://stackoverflow.com/questions/9240235/paypal-ipn-unique-identifier
if ( isset( $transaction_details['payment_status'] ) ) {
$ipn_transaction_id .= '_' . $transaction_details['payment_status'];
}
if ( isset( $transaction_details['ipn_track_id'] ) ) {
$ipn_transaction_id .= '_' . $transaction_details['ipn_track_id'];
}
if ( in_array( $ipn_transaction_id, $handled_transactions ) ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: transaction ' . $ipn_transaction_id . ' has already been correctly handled.' );
exit;
}
// Make sure we're not in the process of handling this IPN request on a server under extreme load and therefore, taking more than a minute to process it (which is the amount of time PayPal allows before resending the IPN request)
$ipn_lock_transient_name = 'wcs_pp_' . $ipn_id; // transient names need to be less than 45 characters and the $ipn_id will be around 30 characters, e.g. subscr_payment_5ab4c38e1f39d
$ipn_lock_transient_name = 'wcs_pp_' . md5( $ipn_transaction_id ); // transient names need to be less than 45 characters and the $ipn_id will be long, e.g. 34292625HU746553V_subscr_payment_completed_5ab4c38e1f39d, so md5
if ( 'in-progress' == get_transient( $ipn_lock_transient_name ) && 'recurring_payment_suspended_due_to_max_failed_payment' !== $transaction_details['txn_type'] ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: an older IPN request with ID ' . $ipn_id . ' is still in progress.' );
WC_Gateway_Paypal::log( 'Subscription IPN Error: an older IPN request with ID ' . $ipn_transaction_id . ' is still in progress.' );
// We need to send an error code to make sure PayPal does retry the IPN after our lock expires, in case something is actually going wrong and the server isn't just taking a long time to process the request
status_header( 503 );
@@ -134,33 +147,6 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
// Set a transient to block IPNs with this transaction ID for the next 4 days (An IPN message may be present in PayPal up to 4 days after the original was sent)
set_transient( $ipn_lock_transient_name, 'in-progress', apply_filters( 'woocommerce_subscriptions_paypal_ipn_request_lock_time', 4 * DAY_IN_SECONDS ) );
}
if ( isset( $transaction_details['txn_id'] ) ) {
// Make sure the IPN request has not already been handled
$handled_transactions = get_post_meta( $subscription->get_id(), '_paypal_transaction_ids', true );
if ( empty( $handled_transactions ) ) {
$handled_transactions = array();
}
$transaction_id = $transaction_details['txn_id'];
if ( isset( $transaction_details['txn_type'] ) ) {
$transaction_id .= '_' . $transaction_details['txn_type'];
}
// The same transaction ID is used for different payment statuses, so make sure we handle it only once. See: http://stackoverflow.com/questions/9240235/paypal-ipn-unique-identifier
if ( isset( $transaction_details['payment_status'] ) ) {
$transaction_id .= '_' . $transaction_details['payment_status'];
}
if ( in_array( $transaction_id, $handled_transactions ) ) {
WC_Gateway_Paypal::log( 'Subscription IPN Error: transaction ' . $transaction_id . ' has already been correctly handled.' );
exit;
}
}
$is_renewal_sign_up_after_failure = false;
@@ -491,14 +477,9 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
}
// Store the transaction IDs to avoid handling requests duplicated by PayPal
if ( isset( $transaction_details['ipn_track_id'] ) ) {
$handled_ipn_requests[] = $ipn_id;
update_post_meta( $subscription->get_id(), '_paypal_ipn_tracking_ids', $handled_ipn_requests );
}
if ( isset( $transaction_details['txn_id'] ) ) {
$handled_transactions[] = $transaction_id;
update_post_meta( $subscription->get_id(), '_paypal_transaction_ids', $handled_transactions );
$handled_transactions[] = $ipn_transaction_id;
update_post_meta( $subscription->get_id(), '_paypal_ipn_tracking_ids', $handled_transactions );
}
// And delete the transient that's preventing other IPN's being processed

View File

@@ -33,13 +33,15 @@ class WCS_PayPal_Standard_Request {
// Payment method changes act on the subscription not the original order
if ( $is_payment_change ) {
$subscriptions = array( wcs_get_subscription( wcs_get_objects_property( $order, 'id' ) ) );
$subscription = array_pop( $subscriptions );
$subscription = wcs_get_subscription( wcs_get_objects_property( $order, 'id' ) );
$order = $subscription->get_parent();
// We need the subscription's total
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
remove_filter( 'woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2 );
} else {
remove_filter( 'woocommerce_subscription_get_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2 );
}
} else {
// Otherwise the order is the $order
@@ -59,9 +61,6 @@ class WCS_PayPal_Standard_Request {
// It's a subscription
$paypal_args['cmd'] = '_xclick-subscriptions';
// Store the subscription ID in the args sent to PayPal so we can access them later
$paypal_args['custom'] = wcs_json_encode( array( 'order_id' => wcs_get_objects_property( $order, 'id' ), 'order_key' => wcs_get_objects_property( $order, 'order_key' ), 'subscription_id' => $subscription->get_id(), 'subscription_key' => $subscription->get_order_key() ) );
foreach ( $subscription->get_items() as $item ) {
if ( $item['qty'] > 1 ) {
$item_names[] = $item['qty'] . ' x ' . wcs_get_paypal_item_name( $item['name'] );
@@ -70,8 +69,14 @@ class WCS_PayPal_Standard_Request {
}
}
// Subscription imported or manually added via admin so doesn't have a parent order
if ( empty( $order ) ) {
// translators: 1$: subscription ID, 2$: names of items, comma separated
$paypal_args['item_name'] = wcs_get_paypal_item_name( sprintf( _x( 'Subscription %1$s - %2$s', 'item name sent to paypal', 'woocommerce-subscriptions' ), $subscription->get_order_number(), implode( ', ', $item_names ) ) );
} else {
// translators: 1$: subscription ID, 2$: order ID, 3$: names of items, comma separated
$paypal_args['item_name'] = wcs_get_paypal_item_name( sprintf( _x( 'Subscription %1$s (Order %2$s) - %3$s', 'item name sent to paypal', 'woocommerce-subscriptions' ), $subscription->get_order_number(), $order->get_order_number(), implode( ', ', $item_names ) ) );
}
$unconverted_periods = array(
'billing_period' => $subscription->get_billing_period(),
@@ -147,6 +152,10 @@ class WCS_PayPal_Standard_Request {
$paypal_args['invoice'] = WCS_PayPal::get_option( 'invoice_prefix' ) . $order_number . $suffix;
$paypal_args['custom'] = wcs_json_encode( array_merge( $order_id_key, array( 'subscription_id' => $subscription->get_id(), 'subscription_key' => $subscription->get_order_key() ) ) );
} else {
// Store the subscription ID in the args sent to PayPal so we can access them later
$paypal_args['custom'] = wcs_json_encode( array( 'order_id' => wcs_get_objects_property( $order, 'id' ), 'order_key' => wcs_get_objects_property( $order, 'order_key' ), 'subscription_id' => $subscription->get_id(), 'subscription_key' => $subscription->get_order_key() ) );
}
if ( $order_contains_failed_renewal ) {
@@ -264,7 +273,11 @@ class WCS_PayPal_Standard_Request {
// Reattach the filter we removed earlier
if ( $is_payment_change ) {
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
add_filter( 'woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2 );
} else {
add_filter( 'woocommerce_subscription_get_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2 );
}
}
}

View File

@@ -539,7 +539,8 @@ class WC_Subscription_Legacy extends WC_Subscription {
public function set_status( $new_status, $note = '', $manual_update = false ) {
$old_status = $this->get_status();
$new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status;
$prefix = substr( $new_status, 0, 3 );
$new_status = 'wc-' === $prefix ? substr( $new_status, 3 ) : $new_status;
wp_update_post( array( 'ID' => $this->get_id(), 'post_status' => wcs_maybe_prefix_key( $new_status, 'wc-' ) ) );
$this->post_status = $this->post->post_status = wcs_maybe_prefix_key( $new_status, 'wc-' );
@@ -746,4 +747,14 @@ class WC_Subscription_Legacy extends WC_Subscription {
update_post_meta( $this->get_id(), $key, $value );
}
}
/**
* Save subscription date changes to the database.
* Nothing to do here as all date properties are saved when calling @see $this->set_prop().
*
* @since 2.2.6
*/
public function save_dates() {
// Nothing to do here.
}
}

View File

@@ -5,24 +5,24 @@ Plugin URI: https://github.com/prospress/action-scheduler
Description: A robust action scheduler for WordPress
Author: Prospress
Author URI: http://prospress.com/
Version: 1.5.2
Version: 1.5.3
*/
if ( ! function_exists( 'action_scheduler_register_1_dot_5_dot_2' ) ) {
if ( ! function_exists( 'action_scheduler_register_1_dot_5_dot_3' ) ) {
if ( ! class_exists( 'ActionScheduler_Versions' ) ) {
require_once( 'classes/ActionScheduler_Versions.php' );
add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 );
}
add_action( 'plugins_loaded', 'action_scheduler_register_1_dot_5_dot_2', 0, 0 );
add_action( 'plugins_loaded', 'action_scheduler_register_1_dot_5_dot_3', 0, 0 );
function action_scheduler_register_1_dot_5_dot_2() {
function action_scheduler_register_1_dot_5_dot_3() {
$versions = ActionScheduler_Versions::instance();
$versions->register( '1.5.2', 'action_scheduler_initialize_1_dot_5_dot_2' );
$versions->register( '1.5.3', 'action_scheduler_initialize_1_dot_5_dot_3' );
}
function action_scheduler_initialize_1_dot_5_dot_2() {
function action_scheduler_initialize_1_dot_5_dot_3() {
require_once( 'classes/ActionScheduler.php' );
ActionScheduler::init( __FILE__ );
}

View File

@@ -29,7 +29,7 @@ class ActionScheduler_AdminView {
*/
public function init() {
if ( defined( 'WP_DEBUG' ) && true == WP_DEBUG && is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) {
if ( is_admin() && ( ! defined( 'DOING_AJAX' ) || false == DOING_AJAX ) ) {
add_filter( 'action_scheduler_post_type_args', array( self::instance(), 'action_scheduler_post_type_args' ) );
}
@@ -63,7 +63,7 @@ class ActionScheduler_AdminView {
'show_ui' => true,
'show_in_menu' => 'tools.php',
'show_in_admin_bar' => false,
));
) );
}
/**

View File

@@ -0,0 +1,9 @@
{
"name": "prospress/action-scheduler",
"version": "1.5.3",
"description": "Action Scheduler for WordPress and WooCommerce",
"type": "wordpress-plugin",
"license": "GPL-3.0",
"minimum-stability": "dev",
"require": {}
}

View File

@@ -89,14 +89,12 @@ class WCS_Retry_Rules {
$rule = null;
if ( isset( $this->default_retry_rules[ $retry_number ] ) ) {
$rule_array = apply_filters( 'wcs_get_retry_rule_raw', $this->default_retry_rules[ $retry_number ], $retry_number, $order_id );
$rule_array = ( isset( $this->default_retry_rules[ $retry_number ] ) ) ? $this->default_retry_rules[ $retry_number ] : array();
$rule_array = apply_filters( 'wcs_get_retry_rule_raw', $rule_array, $retry_number, $order_id );
if ( ! empty( $rule_array ) ) {
$rule = new $this->retry_rule_class( $rule_array );
}
}
return apply_filters( 'wcs_get_retry_rule', $rule, $retry_number, $order_id );
}

View File

@@ -88,6 +88,8 @@ class WC_Subscriptions_Upgrader {
// Sometimes redirect to the Welcome/About page after an upgrade
add_action( 'woocommerce_subscriptions_upgraded', __CLASS__ . '::maybe_redirect_after_upgrade_complete', 100, 2 );
add_action( 'wcs_repair_end_of_prepaid_term_actions', __CLASS__ . '::repair_end_of_prepaid_term_actions' );
}
/**
@@ -184,6 +186,12 @@ class WC_Subscriptions_Upgrader {
do_action( 'woocommerce_subscriptions_reports_schedule_cache_updates' );
}
// Repair missing end_of_prepaid_term scheduled actions
if ( version_compare( self::$active_version, '2.2.0', '>=' ) && version_compare( self::$active_version, '2.2.7', '<' ) ) {
include_once( 'class-wcs-upgrade-2-2-7.php' );
WCS_Upgrade_2_2_7::schedule_end_of_prepaid_term_repair();
}
self::upgrade_complete();
}
@@ -730,6 +738,16 @@ class WC_Subscriptions_Upgrader {
}
}
/**
* Run the end of prepaid term repair script.
*
* @since 2.2.7
*/
public static function repair_end_of_prepaid_term_actions() {
include_once( 'class-wcs-upgrade-2-2-7.php' );
WCS_Upgrade_2_2_7::repair_pending_cancelled_subscriptions();
}
/**
* Used to check if a user ID is greater than the last user upgraded to version 1.4.
*

View File

@@ -64,8 +64,7 @@ class WCS_Upgrade_1_2 {
$sign_up_fee_total = WC_Subscriptions_Order::get_meta( $order, '_sign_up_fee_total', 0 );
// Create recurring_* meta data from existing cart totals
$cart_discount = $order->get_cart_discount();
$cart_discount = $order->get_total_discount();
update_post_meta( $order_id, '_order_recurring_discount_cart', $cart_discount );
$order_discount = $order->get_order_discount();

View File

@@ -0,0 +1,126 @@
<?php
/**
* Repair missing end_of_prepaid_term scheduled actions caused by race conditions when saving subscription date properties post WCS 2.2.0
*
* @author Prospress
* @category Admin
* @package WooCommerce Subscriptions/Admin/Upgrades
* @version 2.2.7
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Upgrade_2_2_7 {
private static $cron_hook = 'wcs_repair_end_of_prepaid_term_actions';
private static $batch_size = 30;
/**
* Schedule an WP-Cron event to run in 5 minutes to repair pending cancelled subscriptions.
*
* @since 2.2.7
*/
public static function schedule_end_of_prepaid_term_repair() {
wp_schedule_single_event( gmdate( 'U' ) + ( MINUTE_IN_SECONDS * 3 ), self::$cron_hook );
}
/**
* Repair a batch of pending cancelled subscriptions.
*
* Subscriptions 2.2.0 included a race condition which causes cancelled subscriptions to not schedule
* end of prepaid term actions. This results in pending cancelled subscriptions not transitioning to
* cancelled automatically.
*
* @since 2.2.7
*/
public static function repair_pending_cancelled_subscriptions() {
$subscriptions_to_repair = self::get_subscriptions_to_repair();
$end_of_prepaid_term_hook = apply_filters( 'woocommerce_subscriptions_scheduled_action_hook', 'woocommerce_scheduled_subscription_end_of_prepaid_term', 'end' );
// Unhook emails to prevent a bunch of Cancelled Subscription emails being sent to Admin
remove_action( 'woocommerce_subscription_status_updated', 'WC_Subscriptions_Email::send_cancelled_email', 10 );
foreach ( $subscriptions_to_repair as $subscription_id ) {
try {
$subscription = wcs_get_subscription( $subscription_id );
if ( false === $subscription ) {
throw new Exception( 'Failed to instantiate subscription object' );
}
$end_time = $subscription->get_time( 'end' );
// End date is in the past, this was likely because the end of prepaid term hook wasn't scheduled - cancel the subscription now
if ( gmdate( 'U' ) > $end_time ) {
self::log( sprintf( 'Subscription %d end date is in the past - cancelling now', $subscription_id ) );
$subscription->update_status( 'cancelled', __( 'Subscription end date in the past', 'woocommerce-subscriptions' ) );
} else {
$action_args = array( 'subscription_id' => $subscription_id );
$scheduled_action = wc_next_scheduled_action( $end_of_prepaid_term_hook, $action_args );
// If there isn't a scheduled end of prepaid term, schedule one now.
if ( false == $scheduled_action ) {
self::log( sprintf( 'Subscription %d missing scheduled end of prepaid term action - scheduled new action (end timestamp: %d)', $subscription_id, $end_time ) );
wc_schedule_single_action( $end_time, $end_of_prepaid_term_hook, $action_args );
} else {
self::log( sprintf( 'Subscription %d has a scheduled end of prepaid term action - there\'s nothing to do here', $subscription_id ) );
}
}
// Set a flag so we don't pull this subscription into a following batch
update_post_meta( $subscription_id, '_wcs_2_2_7_repaired', 'true' );
} catch ( Exception $e ) {
self::log( sprintf( '--- Exception caught repairing subscription %d - exception message: %s ---', $subscription_id, $e->getMessage() ) );
update_post_meta( $subscription_id, '_wcs_2_2_7_repaired', 'false' );
}
}
// Reattach the cancelled subscription emails
add_action( 'woocommerce_subscription_status_updated', 'WC_Subscriptions_Email::send_cancelled_email', 10, 2 );
// If we've processed a full batch, schedule the next batch to be repaired
if ( count( $subscriptions_to_repair ) == self::$batch_size ) {
self::schedule_end_of_prepaid_term_repair();
} else {
self::log( '2.2.7 repair missing end of prepaid terms complete' );
}
}
/**
* Get a batch of pending cancelled subscriptions to repair.
*
* @since 2.2.7
* @return array An list of subscription ids which may need to be repaired.
*/
public static function get_subscriptions_to_repair() {
$subscriptions_to_repair = get_posts( array(
'post_type' => 'shop_subscription',
'post_status' => 'wc-pending-cancel',
'posts_per_page' => self::$batch_size,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => '_wcs_2_2_7_repaired',
'compare' => 'NOT EXISTS',
),
),
) );
return $subscriptions_to_repair;
}
/**
* Add a message to the wcs-upgrade-end-of-prepaid-term-repair log
*
* @param string The message to be logged
* @since 2.2.7
*/
protected static function log( $message ) {
WCS_Upgrade_Logger::add( $message, 'wcs-upgrade-end-of-prepaid-term-repair' );
}
}

View File

@@ -26,8 +26,6 @@ class WCS_Upgrade_Logger {
public static function init() {
add_action( 'woocommerce_subscriptions_upgraded', __CLASS__ . '::schedule_cleanup' );
add_action( 'woocommerce_subscriptions_clear_upgrade_log', __CLASS__ . '::clear' );
}
/**
@@ -35,11 +33,13 @@ class WCS_Upgrade_Logger {
*
* @param string $message
*/
public static function add( $message ) {
public static function add( $message, $handle = '' ) {
$handle = ( '' === $handle ) ? self::$handle : $handle;
if ( empty( self::$log ) ) {
self::$log = new WC_Logger(); // can't use __get() no a static property unfortunately
}
self::$log->add( self::$handle, $message );
self::$log->add( $handle, $message );
}
/**
@@ -67,9 +67,7 @@ class WCS_Upgrade_Logger {
* Schedule a hook to automatically clear the log after 8 weeks
*/
public static function schedule_cleanup() {
$time_to_cleanup = gmdate( 'U' ) + self::$weeks_until_cleanup * WEEK_IN_SECONDS;
self::add( sprintf( 'Upgrade complete. Scheduling log cleanup for %s GMT/UTC', gmdate( 'Y-m-d H:i:s', $time_to_cleanup ) ) );
wc_schedule_single_action( $time_to_cleanup, 'woocommerce_subscriptions_clear_upgrade_log' );
self::add( sprintf( '%s upgrade complete.', WC_Subscriptions::$version ) );
}
}
WCS_Upgrade_Logger::init();

View File

@@ -181,7 +181,7 @@ function wcs_cart_totals_shipping_method_price_label( $method, $cart ) {
$price_label .= ' <small>' . WC()->countries->inc_tax_or_vat() . '</small>';
}
}
} else {
} elseif ( ! empty( $cart->recurring_cart_key ) ) {
$price_label .= _x( 'Free', 'shipping method price', 'woocommerce-subscriptions' );
}

View File

@@ -195,7 +195,7 @@ function wcs_get_objects_property( $object, $property, $single = 'single', $defa
}
} elseif ( 'single' === $single && isset( $object->$property ) ) { // WC < 3.0
$value = $object->$property;
} elseif ( 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 ( 'single' === $single ) {
$value = get_post_meta( wcs_get_objects_property( $object, 'id' ), $prefixed_key, true );
@@ -344,7 +344,7 @@ function wcs_is_order( $order ) {
if ( method_exists( $order, 'get_type' ) ) {
$is_order = ( 'shop_order' === $order->get_type() );
} else {
$is_order = ( 'simple' === $order->order_type );
$is_order = ( isset( $order->order_type ) && 'simple' === $order->order_type );
}
return $is_order;
@@ -451,6 +451,7 @@ function wcs_get_coupon_property( $coupon, $property ) {
'exclude_product_categories' => 'get_excluded_product_categories',
'customer_email' => 'get_email_restrictions',
'enable_free_shipping' => 'get_free_shipping',
'coupon_amount' => 'get_amount',
);
switch ( true ) {
@@ -493,6 +494,7 @@ function wcs_set_coupon_property( &$coupon, $property, $value ) {
'exclude_product_categories' => 'set_excluded_product_categories',
'customer_email' => 'set_email_restrictions',
'enable_free_shipping' => 'set_free_shipping',
'coupon_amount' => 'set_amount',
);
switch ( true ) {

View File

@@ -179,9 +179,9 @@ function wcs_maybe_prefix_key( $key, $prefix = '_' ) {
*/
function wcs_get_calling_function_name() {
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 3 );
$backtrace = version_compare( phpversion(), '5.4.0', '>=' ) ? debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 3 ) : debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // the 2nd param for debug_backtrace() was added in PHP 5.4
$calling_function = isset( $backtrace[2]['class'] ) ? $backtrace[2]['class'] : '';
$calling_function .= isset( $backtrace[2]['type'] ) ? $backtrace[2]['type'] : '';
$calling_function .= isset( $backtrace[2]['type'] ) ? ( ( '->' == $backtrace[2]['type'] ) ? '::' : $backtrace[2]['type'] ) : ''; // Ternary abuses
$calling_function .= isset( $backtrace[2]['function'] ) ? $backtrace[2]['function'] : '';
return $calling_function;

View File

@@ -355,7 +355,7 @@ function wcs_validate_new_order_type( $type ) {
* @return array
*/
function wcs_get_order_address( $order, $address_type = 'shipping' ) {
if ( ! is_object( $order ) ) {
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
return array();
}
@@ -405,8 +405,8 @@ function wcs_order_contains_subscription( $order, $order_type = array( 'parent',
$order_type = array( $order_type );
}
if ( ! is_object( $order ) ) {
$order = new WC_Order( $order );
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}
$contains_subscription = false;

View File

@@ -27,11 +27,13 @@ function wcs_get_price_including_tax( $product, $args = array() ) {
if ( function_exists( 'wc_get_price_including_tax' ) ) { // WC 3.0+
$price = wc_get_price_including_tax( $product, $args );
$filter = 'woocommerce_product_get_price';
} else { // WC < 3.0
$price = $product->get_price_including_tax( $args['qty'], $args['price'] );
$filter = 'woocommerce_get_price';
}
return $price;
return apply_filters( $filter, $price , $product );
}
/**
@@ -51,11 +53,13 @@ function wcs_get_price_excluding_tax( $product, $args = array() ) {
if ( function_exists( 'wc_get_price_excluding_tax' ) ) { // WC 3.0+
$price = wc_get_price_excluding_tax( $product, $args );
$filter = 'woocommerce_product_get_price';
} else { // WC < 3.0
$price = $product->get_price_excluding_tax( $args['qty'], $args['price'] );
$filter = 'woocommerce_get_price';
}
return $price;
return apply_filters( $filter, $price , $product );
}
/**
@@ -82,22 +86,12 @@ function wcs_get_price_html_from_text( $product = '' ) {
*/
function wcs_get_variation_prices( $variation, $variable_product ) {
if ( WC_Subscriptions::is_woocommerce_pre( '3.0' ) ) {
$regular_price = $variation->regular_price;
$sale_price = $variation->sale_price;
} else {
$regular_price = $variation->get_regular_price( 'edit' );
$sale_price = $variation->get_sale_price( 'edit' );
}
$prices = array(
return array(
'price' => apply_filters( 'woocommerce_variation_prices_price', WC_Subscriptions_Product::get_price( $variation ), $variation, $variable_product ),
'regular_price' => apply_filters( 'woocommerce_variation_prices_regular_price', WC_Subscriptions_Product::get_regular_price( $variation, 'edit' ), $variation, $variable_product ),
'sale_price' => apply_filters( 'woocommerce_variation_prices_sale_price', WC_Subscriptions_Product::get_sale_price( $variation, 'edit' ), $variation, $variable_product ),
'sign_up_fee' => apply_filters( 'woocommerce_variation_prices_sign_up_fee', WC_Subscriptions_Product::get_sign_up_fee( $variation ), $variation, $variable_product ),
);
return $prices;
}
/**
@@ -171,7 +165,7 @@ function wcs_calculate_min_max_variations( $variations_data ) {
$is_max = $is_min = false;
if ( empty( $variation_data['price'] ) && empty( $variation_data['subscription']['sign_up_fee'] ) ) {
if ( '' === $variation_data['price'] && '' === $variation_data['subscription']['sign_up_fee'] ) {
continue;
}

View File

@@ -45,7 +45,7 @@ function wcs_create_renewal_order( $subscription ) {
*/
function wcs_order_contains_renewal( $order ) {
if ( ! is_object( $order ) ) {
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}
@@ -111,14 +111,14 @@ function wcs_cart_contains_failed_renewal_order_payment() {
*/
function wcs_get_subscriptions_for_renewal_order( $order ) {
if ( ! is_object( $order ) ) {
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}
$subscriptions = array();
// Only use the order if we actually found a valid order object
if ( is_object( $order ) ) {
if ( is_a( $order, 'WC_Abstract_Order' ) ) {
$subscription_ids = wcs_get_objects_property( $order, 'subscription_renewal', 'multiple' );
foreach ( $subscription_ids as $subscription_id ) {

View File

@@ -22,8 +22,8 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
function wcs_order_contains_resubscribe( $order ) {
if ( ! is_object( $order ) ) {
$order = new WC_Order( $order );
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}
if ( wcs_get_objects_property( $order, 'subscription_resubscribe' ) ) {
@@ -138,7 +138,7 @@ function wcs_cart_contains_resubscribe( $cart = '' ) {
*/
function wcs_get_subscriptions_for_resubscribe_order( $order ) {
if ( ! is_object( $order ) ) {
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}

View File

@@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
function wcs_order_contains_switch( $order ) {
if ( ! is_object( $order ) ) {
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}
@@ -30,9 +30,9 @@ function wcs_order_contains_switch( $order ) {
} else {
$subscription_ids = wcs_get_objects_property( $order, 'subscription_switch', 'multiple' );
$switched_subscriptions = wcs_get_subscriptions_for_switch_order( $order );
if ( ! empty( $subscription_ids ) ) {
if ( ! empty( $switched_subscriptions ) ) {
$is_switch_order = true;
} else {
$is_switch_order = false;
@@ -51,7 +51,7 @@ function wcs_order_contains_switch( $order ) {
*/
function wcs_get_subscriptions_for_switch_order( $order ) {
if ( ! is_object( $order ) ) {
if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}
@@ -59,7 +59,12 @@ function wcs_get_subscriptions_for_switch_order( $order ) {
$subscription_ids = wcs_get_objects_property( $order, 'subscription_switch', 'multiple' );
foreach ( $subscription_ids as $subscription_id ) {
$subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id );
$subscription = wcs_get_subscription( $subscription_id );
if ( $subscription ) {
$subscriptions[ $subscription_id ] = $subscription;
}
}
return $subscriptions;

View File

@@ -172,7 +172,11 @@ function wcs_get_users_subscriptions( $user_id = 0 ) {
) );
foreach ( $post_ids as $post_id ) {
$subscriptions[ $post_id ] = wcs_get_subscription( $post_id );
$subscription = wcs_get_subscription( $post_id );
if ( $subscription ) {
$subscriptions[ $post_id ] = $subscription;
}
}
}
@@ -282,7 +286,7 @@ function wcs_get_all_user_actions_for_subscription( $subscription, $user_id ) {
// Show button for subscriptions which can be cancelled and which may actually require cancellation (i.e. has a future payment)
$next_payment = $subscription->get_time( 'next_payment' );
if ( $subscription->can_be_updated_to( 'cancelled' ) && ! $subscription->is_one_payment() && ( $next_payment > 0 || ( $subscription->has_status( 'on-hold' ) && empty( $next_payment ) ) ) ) {
if ( $subscription->can_be_updated_to( 'cancelled' ) && ( ! $subscription->is_one_payment() && ( $subscription->has_status( 'on-hold' ) && empty( $next_payment ) ) || $next_payment > 0 ) ) {
$actions['cancel'] = array(
'url' => wcs_get_users_change_status_link( $subscription->get_id(), 'cancelled', $current_status ),
'name' => _x( 'Cancel', 'an action on a subscription', 'woocommerce-subscriptions' ),

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
// Subscription Price
woocommerce_wp_text_input( array(
'id' => 'variable_subscription_price[' . $loop . ']',
'class' => 'wc_input_subscription_price',
'class' => 'wc_input_subscription_price wc_input_price',
'wrapper_class' => '_subscription_price_field',
// translators: placeholder is a currency symbol / code
'label' => sprintf( __( 'Subscription Price (%s)', 'woocommerce-subscriptions' ), get_woocommerce_currency_symbol() ),

View File

@@ -16,7 +16,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<div class="variable_subscription_trial variable_subscription_pricing_2_3 show_if_variable-subscription variable_subscription_trial_sign_up">
<p class="form-row form-row-first form-field show_if_variable-subscription sign-up-fee-cell">
<label for="variable_subscription_sign_up_fee[<?php echo esc_attr( $loop ); ?>]"><?php printf( esc_html__( 'Sign-up fee (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) ); ?></label>
<input type="text" class="wc_input_subscription_intial_price" name="variable_subscription_sign_up_fee[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( WC_Subscriptions_Product::get_sign_up_fee( $variation_product ) ); ?>" placeholder="<?php echo esc_attr_x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ); ?>">
<input type="text" class="wc_input_price wc_input_subscription_intial_price" name="variable_subscription_sign_up_fee[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( WC_Subscriptions_Product::get_sign_up_fee( $variation_product ) ); ?>" placeholder="<?php echo esc_attr_x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ); ?>">
</p>
<p class="form-row form-row-last show_if_variable-subscription">
<label for="variable_subscription_trial_length[<?php echo esc_attr( $loop ); ?>]">
@@ -42,7 +42,7 @@ if ( ! defined( 'ABSPATH' ) ) {
printf( esc_html__( 'Subscription price (%s)', 'woocommerce-subscriptions' ), esc_html( get_woocommerce_currency_symbol() ) );
?>
</label>
<input type="text" class="wc_input_subscription_price" name="variable_subscription_price[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( WC_Subscriptions_Product::get_price( $variation_product ) ); ?>" placeholder="<?php echo esc_attr_x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ); ?>">
<input type="text" class="wc_input_price wc_input_subscription_price" name="variable_subscription_price[<?php echo esc_attr( $loop ); ?>]" value="<?php echo esc_attr( WC_Subscriptions_Product::get_price( $variation_product ) ); ?>" placeholder="<?php echo esc_attr_x( 'e.g. 9.90', 'example price', 'woocommerce-subscriptions' ); ?>">
<label for="variable_subscription_period_interval[<?php echo esc_attr( $loop ); ?>]" class="wcs_hidden_label"><?php esc_html_e( 'Billing interval:', 'woocommerce-subscriptions' ); ?></label>
<select name="variable_subscription_period_interval[<?php echo esc_attr( $loop ); ?>]" class="wc_input_subscription_period_interval">

View File

@@ -5,7 +5,7 @@
* Description: Sell products and services with recurring payments in your WooCommerce Store.
* Author: Prospress Inc.
* Author URI: http://prospress.com/
* Version: 2.2.4
* Version: 2.2.7
*
* Copyright 2016 Prospress, Inc. (email : freedoms@prospress.com)
*
@@ -126,7 +126,7 @@ class WC_Subscriptions {
public static $plugin_file = __FILE__;
public static $version = '2.2.4';
public static $version = '2.2.7';
private static $total_subscription_count = null;
@@ -721,6 +721,8 @@ class WC_Subscriptions {
require_once( 'includes/class-wcs-user-change-status-handler.php' );
require_once( 'includes/class-wcs-my-account-payment-methods.php' );
if ( self::is_woocommerce_pre( '3.0' ) ) {
require_once( 'includes/legacy/class-wc-subscription-legacy.php' );
@@ -737,10 +739,8 @@ class WC_Subscriptions {
if ( ! class_exists( 'WC_DateTime' ) ) {
require_once( 'includes/libraries/class-wc-datetime.php' );
}
} else {
require_once( 'includes/class-wc-order-item-pending-switch.php');
require_once( 'includes/class-wc-order-item-pending-switch.php' );
require_once( 'includes/data-stores/class-wcs-subscription-data-store-cpt.php' );