WooCommerce Subscriptions: not able to purchase subscription after failed payment [edited on 2021-01-27]


This is a workaround and fix for the following situation which occurs in recent versions (<3.0.12) of WooCommerce Subscriptions. When purchasing a subscription which is limited to “any” (ie a user can only have one subscription of any status), the user cannot pay for a failed order as the subscription product gets removed from their basket.

If the order is an initial purchase where the payment for the parent order for a subscription has failed, the user or support team can work around this by setting the order status to cancelled which will allow the user to purchase the product again. If the order is a resubscribe, cancelling the resubscribe order with the failed payment does not work.

Reproduce

To reproduce the parent order issue:

  • Create an order with a failed status containing a subscription product which is limited to one of any status per user.
  • In the account pages of the user whose order it is, try to pay for that order. This will fail.
  • Cancel the order and try to purchase a new one. This will succeed.

To reproduce the resubscribe issues:

  • On a subscription which has been cancelled and to which the user can resubscribe, create a resubscribe order and set the order status to failed.
  • In the account pages of the user whose order it is, try to pay for that order. This will fail.
  • Cancel the order. There is now no way to resubscribe to the subscription.

Fixes

There are two fixes necessary for this. For the issue where failed payments remove the subscription from the cart when payment is attempted again, this patch fixes it and will be included in WooCommerce Subscriptions 3.0.13 (or whatever is after 3.0.12!). (Note to self: is there a diff highlighter in Prism? Add it if so.)

PHP

diff --git a/includes/class-wcs-limiter.php b/includes/class-wcs-limiter.php
index d030723..4fa1e8d 100644
--- a/includes/class-wcs-limiter.php
+++ b/includes/class-wcs-limiter.php
@@ -253,7 +253,7 @@ class WCS_Limiter {
 			foreach ( $order->get_items() as $item ) {
 
 				// If this order contains the product we're interested in, continue finding a related subscription.
-				if ( $item['product_id'] == $product_id && $item['variation_id'] == $product_id ) {
+				if ( $item['product_id'] == $product_id || $item['variation_id'] == $product_id ) {
 					$subscriptions = wcs_get_subscriptions(
 						array(
 							'order_id'            => $order->get_id(),

As a workaround before the patched version of Woo Subs is released, cancelling the parent order allows the user to purchase a subscription again. However, this does not work for resubscribes as there is a flaw in the logic in the check.

The patch for this is here and you can also use the filter in that function to work around it as below. This is simply applying the linked patch in a filter after the WCS function has run.

PHP
/**
 * Allow users to resubscribe to the parent order when the resubscribe order (and therefore subscription) is cancelled.
 * This function basically redoes the wcs_can_user_resubscribe_to() function in WooCommerce Subscriptions.
 *
 * @param bool                $can_user_resubscribe If the user is allowed to resubscribe.
 * @param int|WC_Subscription $subscription The subscription which should be allowed a resubscribe.
 * @param int                 $user_id The id of the user.
 * @return bool Whether or not the user can resubscribe.
 */
public function allow_resubscribe_on_failed_order( $can_user_resubscribe, $subscription, $user_id ) {
	// We are not bailing early as we have added to the product limitations condition. You cannot resubscribe if you have an on hold or active subscription.
	// Otherwise let's run through this function again.
	if ( ! is_object( $subscription ) ) {
		$subscription = wcs_get_subscription( $subscription );
	}
	if ( empty( $user_id ) ) {
		$user_id = get_current_user_id();
	}
	if ( empty( $subscription ) ) {
		$can_user_resubscribe = false;
	} elseif ( ! user_can( $user_id, 'subscribe_again', $subscription->get_id() ) ) { // Note: This checks that the user is the same as the user on the subscription.
		$can_user_resubscribe = false;
	} elseif ( ! $subscription->has_status( array( 'pending-cancel', 'cancelled', 'expired', 'trash' ) ) ) {
		$can_user_resubscribe = false;
	} elseif ( $subscription->get_total() <= 0 ) {
		$can_user_resubscribe = false;
	} else {
		$resubscribe_order_ids = $subscription->get_related_orders( 'ids', 'resubscribe' );
		// Make sure all line items still exist
		$all_line_items_exist = true;
		// Check if product in subscription is limited
		$has_active_limited_subscription = false;
		foreach ( $subscription->get_items() as $line_item ) {
			$product = ( ! empty( $line_item['variation_id'] ) ) ? wc_get_product( $line_item['variation_id'] ) : wc_get_product( $line_item['product_id'] );
			if ( false === $product ) {
				$all_line_items_exist = false;
				break;
			}
			// Check for active or any as the product limitation. [edited on 2021-01-27]
			$product_limitations = wcs_get_product_limitation( $product );
			if ( 'active' === $product_limitations || 'any' === $product_limitations ) {
				if ( $product->is_type( 'variation' ) ) {
					$limited_product_id = $product->get_parent_id();
				} else {
					$limited_product_id = $product->get_id();
				}
				if ( wcs_user_has_subscription( $user_id, $limited_product_id, 'on-hold' ) || wcs_user_has_subscription( $user_id, $limited_product_id, 'active' ) ) {
					$has_active_limited_subscription = true;
					break;
				}
			}
		}
		// If all the resubscribe orders have payment cancelled and all the subscriptions have a status of cancelled then the user can resubscribe.
		$resubscribe_order_cancelled = true;
		foreach ( $resubscribe_order_ids as $resubscribe_order_id ) {
			$resubscribe_order = wc_get_order( $resubscribe_order_id );
			// Check that $resubscribe_order is a WC_Order object before checking the status. [edited on 2021-01-27]
			if ( is_a( $resubscribe_order, 'WC_Order' ) && ! $resubscribe_order->has_status( 'cancelled' ) ) {
				$resubscribe_order_cancelled = false;
				break;
			}
		}
		if ( ( empty( $resubscribe_order_ids ) || $resubscribe_order_cancelled === true ) && $subscription->get_payment_count() > 0 && true === $all_line_items_exist && false === $has_active_limited_subscription ) {
			$can_user_resubscribe = true;
		} else {
			$can_user_resubscribe = false;
		}
	}
	return $can_user_resubscribe;
}

Edits on 2021-01-27

There was a fatal error in the resubscribe order check as occasionally wc_get_order( $resubscribe_order_id ) returned false. I added in a check to make sure it’s an order before checking the status.

Also, I realised I wanted the limited subscription check to check on_hold and active statuses when the limitation was “any” so I’ve added that in there.

Please see the comments in the code for exactly where both of those were added.

Leave a Reply

Your email address will not be published. Required fields are marked *

By submitting this comment, you are agreeing to the use of Akismet which helps reduce spam. You can view Akismet’s privacy policy here. Your email, website and name are also stored on this site.