Last active
January 18, 2021 01:11
-
-
Save doubleedesign/4b70483c66dde99a9f6f73594c21fb49 to your computer and use it in GitHub Desktop.
Automatically downgrade a WooCommerce subscription (to a free, no-expiry variation) instead of expiring it
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Add base tier option to Advanced tab of subscription product | |
*/ | |
function doublee_subscription_product_advanced_settings() { | |
$product = wc_get_product(get_the_id()); | |
if($product->get_type() === 'variable-subscription') { | |
$variations = $product->get_available_variations(); | |
$variation_options = array(); | |
foreach ($variations as $variation) { | |
$variation_id = $variation['variation_id']; | |
$listing_level = $variation['attributes']['attribute_pa_subscription-level']; | |
$term = get_term_by('slug', $listing_level, 'pa_subscription-level'); | |
$variation_options[$variation_id] = $term->name; | |
} | |
echo '<div class="options_group subscription_base_tier">'; | |
woocommerce_wp_select( array( | |
'id' => 'default_subscription_variation', | |
'value' => get_post_meta( get_the_id(), 'subscription_base_tier', true ), | |
'wrapper_class' => '', | |
'label' => 'Subscription base tier', | |
'description' => 'Level to downgrade to when a subscription expires or is not renewed. This level should be free and have no expiry.', | |
'options' => $variation_options, | |
) ); | |
echo '</div>'; | |
} | |
} | |
add_action('woocommerce_product_options_advanced', 'doublee_subscription_product_advanced_settings'); | |
/** | |
* Save the base tier option to postmeta of the subscription product | |
* @param $post_id | |
*/ | |
function doublee_save_subscription_product_advanced_settings($post_id) { | |
if(isset($_POST['default_subscription_variation'])) { | |
$base_tier = $_POST['default_subscription_variation']; | |
update_post_meta($post_id, 'subscription_base_tier', $base_tier); | |
} | |
} | |
add_action('woocommerce_process_product_meta', 'doublee_save_subscription_product_advanced_settings', 10, 1); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Automatically downgrade a subscription to the free, no-expiry tier when: | |
* - A subscription expires | |
* - Renewal payment fails | |
* - A subscription with manual renewal payment is not renewed | |
* | |
* @param $subscription | |
*/ | |
function doublee_downgrade_subscription_to_base_tier($subscription) { | |
// Set subscription back to active | |
$subscription->update_status('active'); | |
// Get the product associated with the subscription | |
$items = $subscription->get_items(); | |
$product_id = $items[array_key_first($items)]->get_product_id(); | |
// Get the base tier variation from the subscription product's meta | |
$base_tier_variation_id = get_post_meta($product_id, 'subscription_base_tier', true); | |
// Get the full variation array | |
$product = wc_get_product($product_id); | |
$all_variations = $product->get_available_variations(); | |
$base_tier_variation_details = doublee_search_nested_associative_array('variation_id', $base_tier_variation_id, $all_variations); | |
// Get the details we need from the base variation array | |
$subscription_level_slug = $base_tier_variation_details['attributes']['attribute_pa_subscription-level']; | |
$subscription_level_term = get_term_by('slug', $subscription_level_slug, 'pa_subscription-level'); | |
$subscription_level_name = $subscription_level_term->name; | |
// Get the subscription order line item to update | |
$items = $subscription->get_items(); | |
$order_item_index = array_key_first($items); | |
// Remove it | |
$subscription->remove_item($order_item_index); | |
// Replace it with a new item of the base tier | |
$new_order_item = new WC_Order_Item_Product(); | |
$new_order_item->set_product($product); | |
$new_order_item->set_name($product->get_name() . ' - ' . $subscription_level_name); | |
$new_order_item->set_variation_id($base_tier_variation_id); | |
$new_order_item->set_quantity(1); | |
$new_order_item->set_subtotal($base_tier_variation_details['display_regular_price']); | |
$new_order_item->set_total($base_tier_variation_details['display_regular_price']); | |
$new_order_item->save(); | |
$subscription->add_item($new_order_item); | |
// Update the subscription renewal date and clear the end date | |
$dates_to_update = array(); | |
$dates_to_update['end'] = ''; | |
$dates_to_update['next_payment'] = date('Y-m-d H:i:s', strtotime('+1 year')); | |
$subscription->update_dates($dates_to_update); | |
// Update the recurring total of the subscription | |
$subscription->calculate_totals(); | |
// Get the listing post ID from the subscription's post meta | |
$listing_id = doublee_get_listing_for_order_or_subscription($subscription->get_id()); | |
// Update its listing_level taxonomy terms | |
$new_level = get_term_by('slug', $subscription_level_slug, 'listing_level'); | |
wp_set_object_terms($listing_id, $new_level->term_id, 'listing_level'); | |
// Update its listing_level post meta | |
update_post_meta($listing_id, '_listing_level', $subscription_level_slug); | |
} | |
/** | |
* Trigger downgrade after expiry | |
* Note: Added plugin mod to add custom action hook woocommerce_after_subscription_status_updated to ensure the notes are added in the right order | |
* // TODO: Check if that mod can be added to WooCommerce core instead and submit a pull request if it can | |
* @param $subscription | |
* @param $to | |
* @param $from | |
*/ | |
function doublee_downgrade_on_subscription_expiry($subscription, $to, $from) { | |
if($to == 'expired') { | |
// Perform downgrade | |
doublee_downgrade_subscription_to_base_tier($subscription); | |
// Add CMS notes | |
$subscription->add_order_note('Status changed from Expired to ' . ucfirst($subscription->get_status())); | |
$subscription->add_order_note('Subscription automatically downgraded to base tier and reactivated, due to expiry or non-renewal.', 0, false); | |
} | |
} | |
add_action('woocommerce_after_subscription_status_updated', 'doublee_downgrade_on_subscription_expiry', 5, 3); | |
/** | |
* Trigger downgrade on failed renewal payment | |
* | |
* @param $subscription | |
*/ | |
function doublee_downgrade_on_renewal_failure($subscription) { | |
// Perform downgrade | |
doublee_downgrade_subscription_to_base_tier($subscription); | |
// Add CMS note | |
$subscription->add_order_note('Subscription automatically downgraded to base tier and reactivated, due to expiry or non-renewal.', 0, false); | |
} | |
add_action('woocommerce_subscription_renewal_payment_failed', 'doublee_downgrade_on_renewal_failure', 10, 1); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Handle the status transition. | |
*/ | |
protected function status_transition() { | |
// Use local copy of status transition value. | |
$status_transition = $this->status_transition; | |
// If we're not currently in the midst of a status transition, bail early. | |
if ( ! $status_transition ) { | |
return; | |
} | |
// MOD: If the statuses are the same, don't do anything. | |
// doublee_downgrade_subscription_to_base_tier triggers an "Active to Active" order note because it triggers a save() within some of its methods, | |
// which is necessary for the updates that that function makes, but it also happens to trigger status_transition(). | |
if($status_transition['from'] === $status_transition['to']) { | |
return; | |
} | |
try { | |
do_action( "woocommerce_subscription_status_{$status_transition['to']}", $this ); | |
if ( ! empty( $status_transition['from'] ) ) { | |
$transition_note = sprintf( | |
/* translators: 1: old subscription status 2: new subscription status */ | |
__( 'Status changed from %1$s to %2$s.', 'woocommerce-subscriptions' ), | |
wcs_get_subscription_status_name( $status_transition['from'] ), | |
wcs_get_subscription_status_name( $status_transition['to'] ) | |
); | |
do_action( "woocommerce_subscription_status_{$status_transition['from']}_to_{$status_transition['to']}", $this ); | |
// Trigger a hook with params we want. | |
do_action( 'woocommerce_subscription_status_updated', $this, $status_transition['to'], $status_transition['from'] ); | |
// Trigger a hook with params matching WooCommerce's 'woocommerce_order_status_changed' hook so functions attached to it can be attached easily to subscription status changes. | |
do_action( 'woocommerce_subscription_status_changed', $this->get_id(), $status_transition['from'], $status_transition['to'], $this ); | |
} else { | |
/* translators: %s: new order status */ | |
$transition_note = sprintf( __( 'Status set to %s.', 'woocommerce-subscriptions' ), wcs_get_subscription_status_name( $status_transition['to'] ) ); | |
} | |
// Note the transition occurred. | |
$this->add_order_note( trim( "{$status_transition['note']} {$transition_note}" ), 0, $status_transition['manual'] ); | |
// MOD: Custom action hook for some of our custom subscription processing | |
do_action( 'woocommerce_after_subscription_status_updated', $this, $status_transition['to'], $status_transition['from'] ); | |
} catch ( Exception $e ) { | |
$logger = wc_get_logger(); | |
$logger->error( | |
sprintf( 'Status transition of subscription #%d errored!', $this->get_id() ), | |
array( | |
'order' => $this, | |
'error' => $e, | |
) | |
); | |
$this->add_order_note( __( 'Error during subscription status transition.', 'woocommerce-subscriptions' ) . ' ' . $e->getMessage() ); | |
} | |
// This has run, so reset status transition variable | |
$this->status_transition = false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Utility function to find a key => value pair in a nested associative array and then return that array | |
* @param $key | |
* @param $value | |
* @param $haystack | |
* | |
* @return false|mixed | |
*/ | |
function doublee_search_nested_associative_array($key, $value, $haystack) { | |
foreach($haystack as $index => $inner_array) { | |
if($inner_array[$key] == $value) { | |
return $inner_array; | |
} | |
} | |
return false; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment