Skip to content

Instantly share code, notes, and snippets.

@doubleedesign
Last active January 18, 2021 01:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save doubleedesign/4b70483c66dde99a9f6f73594c21fb49 to your computer and use it in GitHub Desktop.
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
<?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);
<?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);
<?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;
}
<?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