2

I'm looking for a way to programmatically add a switch between two Woocommerce subscription variations to the cart. We're building a headless WP site, so I don't want to do it with a link, as described in this question

I've tried the following simple way, but it won't allow it that way, since the user is already subscribed:

WC()->cart->add_to_cart( 1907, 1, 1908 );

I want to mimic what is happening when the user goes to the upgrade/downgrade page, selects a new variation and presses Switch subscription. Usually that means that the user is sent to checkout with the switch in cart, I just want it to be added to the cart, since we have our own checkout.

Punchlinern
  • 714
  • 5
  • 17
  • 33
  • something like this? https://www.businessbloomer.com/woocommerce-custom-add-cart-urls-ultimate-guide/ – GrafiCode Aug 18 '21 at 12:22
  • 1
    @GrafiCode, I would like something that doesn't require clicking a URL since the WP-part is just backend. It's a Gatsby frontend doing a graphql request to WP, that should add the subscription change to the cart. The end user never sees WP. – Punchlinern Aug 18 '21 at 13:38
  • just for clarity, you're using this plugin? `woocommerce subscriptions` https://woocommerce.com/products/woocommerce-subscriptions/ – GrafiCode Aug 18 '21 at 13:44
  • @GrafiCode, Yes, exactly – Punchlinern Aug 18 '21 at 13:58
  • I tried digging the plugin api documentation, and it looks like subscription switching is always related to an `order` object. https://docs.woocommerce.com/document/subscriptions/develop/functions/#section-15 «*A subscription can be purchased in an order, have an order created when it renews, and have an order created to record an upgrade or downgrade.*» Could it mean that you have to programmatically create an order when your customer clicks on your mimicked subscription switch panel? – GrafiCode Aug 19 '21 at 12:34
  • 1
    @GrafiCode Thanks for your persistence! I think I've got it, but I'm still testing some things. I could use add_to_cart(), but I had to convince WC that the product was actually purchasable, and also add some metadata to the cart item. I'll post an answer when I've figured it out completely! – Punchlinern Aug 19 '21 at 12:53

1 Answers1

2

The main issue is that Woocommerce thinks that a subscription product is unpurchaseable when it is added to the cart any other way than through the subscription switch form. The form sends a couple of GET-parameters which sends WC through some extra validation, and then allows the item to be added to the cart if everything else is valid.

My solution to this is to mimic this validation with some functions of my own, mostly stolen from the subscription plugin.

/**
 * Must be set to true before programmatically adding subscription switch to cart.
 *
 * @var  bool
 */
$kbhl_adding_switch_to_cart = false;

/**
 * Array filled with data about current subscription.
 * Should only be retrieved by kbhl_get_current_subscription_data().
 *
 * @var  array
 */
$kbhl_global_current_subscription_data = array();


/**
 * Cache whether a given product is purchasable or not to save running lots of queries for the same product in the same request
 *
 *      $is_purchasable_cache taken from plugins\woocommerce-subscriptions\includes\class-wcs-limiter.php
 *
 * @var  array
 */
$kbhl_purchasable_cache = array();

/**
 * $user_subscriptions_to_product taken from plugins\woocommerce-subscriptions\includes\class-wcs-limiter.php
 *
 * @var  array
 */
$kbhl_user_subscriptions_to_product = array();


/**
 * If a product is being marked as not purchasable because it is limited and the customer has a subscription,
 * but the current request is to switch the subscription, then mark it as purchasable.
 *
 *        Function is_purchasable_switch() taken from plugins\woocommerce-subscriptions\includes\class-wcs-limiter.php
 *
 * @param   bool $is_purchasable  Current purchasable status.
 * @param   obj  $product         Product being checked.
 *
 * @return  bool                  New purchasable status.
 */
function kbhl_is_purchasable_switch( $is_purchasable, $product ) {
    global $kbhl_purchasable_cache;

    $kbhl_current_subscription_data = kbhl_get_current_subscription_data();

    // Only process this filter if running custom add switch function.
    if ( ! empty( $kbhl_current_subscription_data['id'] ) ) {
        return $is_purchasable;
    }

    $product_key = wcs_get_canonical_product_id( $product );

    // Set an empty cache if one isn't set yet.
    if ( ! isset( $kbhl_purchasable_cache[ $product_key ] ) ) {
        $kbhl_purchasable_cache[ $product_key ] = array();
    }

    // Exit early if we've already determined this product's purchasability via switching.
    if ( isset( $kbhl_purchasable_cache[ $product_key ]['switch'] ) ) {
        return $kbhl_purchasable_cache[ $product_key ]['switch'];
    }

    // If the product is already purchasble, we don't need to determine it's purchasibility via switching/auto-switching.
    if ( true === $is_purchasable || ! is_user_logged_in() || ! wcs_is_product_switchable_type( $product ) || ! WC_Subscriptions_Product::is_subscription( $product->get_id() ) ) {
        $kbhl_purchasable_cache[ $product_key ]['switch'] = $is_purchasable;
        return $kbhl_purchasable_cache[ $product_key ]['switch'];
    }

    $user_id            = get_current_user_id();
    $product_limitation = wcs_get_product_limitation( $product );

    if ( 'no' == $product_limitation || ! wcs_user_has_subscription( $user_id, $product->get_id(), wcs_get_product_limitation( $product ) ) ) {
        $kbhl_purchasable_cache[ $product_key ]['switch'] = $is_purchasable;
        return $kbhl_purchasable_cache[ $product_key ]['switch'];
    }

    // Adding to cart.
    if ( array_key_exists( $kbhl_current_subscription_data['id'], kbhl_get_user_subscriptions_to_product( $product, $user_id, $product_limitation ) ) ) {
        $is_purchasable = true;
    }

    $kbhl_purchasable_cache[ $product_key ]['switch'] = $is_purchasable;
    return $kbhl_purchasable_cache[ $product_key ]['switch'];
}
add_filter( 'woocommerce_subscription_is_purchasable', 'kbhl_is_purchasable_switch', 13, 2 );



/**
 * Gets a list of the customer subscriptions to a product with a particular limited status.
 *
 *        Function get_user_subscriptions_to_product() taken from plugins\woocommerce-subscriptions\includes\class-wcs-limiter.php
 *
 * @param WC_Product|int $product      The product object or product ID.
 * @param int            $user_id      The user's ID.
 * @param string         $limit_status The limit status.
 *
 * @return WC_Subscription[] An array of a customer's subscriptions with a specific status and product.
 */
function kbhl_get_user_subscriptions_to_product( $product, $user_id, $limit_status ) {
    global $user_subscriptions_to_product;
    $product_id = is_object( $product ) ? $product->get_id() : $product;
    $cache_key  = "{$product_id}_{$user_id}_{$limit_status}";

    if ( ! isset( $user_subscriptions_to_product[ $cache_key ] ) ) {
        // Getting all the customers subscriptions and removing ones without the product is more performant than querying for subscriptions with the product.
        $subscriptions = wcs_get_subscriptions(
            array(
                'customer_id' => $user_id,
                'status'      => $limit_status,
            )
        );

        foreach ( $subscriptions as $subscription_id => $subscription ) {
            if ( ! $subscription->has_product( $product_id ) ) {
                unset( $subscriptions[ $subscription_id ] );
            }
        }

        $user_subscriptions_to_product[ $cache_key ] = $subscriptions;
    }

    return $user_subscriptions_to_product[ $cache_key ];
}





/**
 * When a subscription switch is added to the cart, store a record of pertinent meta about the switch.
 *
 * @since 1.4
 */

/**
 * When a subscription switch is added to the cart, store a record of pertinent meta about the switch.
 *
 *       Function set_switch_details_in_cart() taken from plugins\woocommerce-subscriptions\includes\class-wc-subscriptions-switcher.php
 *
 * @param   array $cart_item_data    Current cart item data.
 * @param   int   $product_id        ID of current product.
 * @param   int   $variation_id      ID of current product variation.
 *
 * @return  array                   Updated cart item data.
 */
function kbhl_set_switch_details_in_cart( $cart_item_data, $product_id, $variation_id ) {
    try {

        $kbhl_current_subscription_data = kbhl_get_current_subscription_data();
        if ( empty( $kbhl_current_subscription_data['id'] ) || empty( $kbhl_current_subscription_data['item'] ) ) {
            return $cart_item_data;
        }

        $subscription = wcs_get_subscription( $kbhl_current_subscription_data['id'] );

        // Requesting a switch for someone elses subscription.
        if ( ! current_user_can( 'switch_shop_subscription', $subscription->get_id() ) ) {
            wc_add_notice( __( 'You can not switch this subscription. It appears you do not own the subscription.', 'woocommerce-subscriptions' ), 'error' );
            WC()->cart->empty_cart( true );
            return array();
        }

        $item = wcs_get_order_item( absint( $kbhl_current_subscription_data['item'] ), $subscription );

        // Else it's a valid switch.
        $product         = wc_get_product( $item['product_id'] );
        $parent_products = WC_Subscriptions_Product::get_parent_ids( $product );
        $child_products  = array();

        if ( ! empty( $parent_products ) ) {
            foreach ( $parent_products as $parent_id ) {
                $child_products = array_unique( array_merge( $child_products, wc_get_product( $parent_id )->get_children() ) );
            }
        }

        if ( $product_id != $item['product_id'] && ! in_array( $item['product_id'], $child_products ) ) {
            return $cart_item_data;
        }

        $next_payment_timestamp = $subscription->get_time( 'next_payment' );

        // If there are no more payments due on the subscription, because we're in the last billing period, we need to use the subscription's expiration date, not next payment date.
        if ( false == $next_payment_timestamp ) {
            $next_payment_timestamp = $subscription->get_time( 'end' );
        }

        $cart_item_data['subscription_switch'] = array(
            'subscription_id'        => $subscription->get_id(),
            'item_id'                => absint( $kbhl_current_subscription_data['item'] ),
            'next_payment_timestamp' => $next_payment_timestamp,
            'upgraded_or_downgraded' => '',
        );

        return $cart_item_data;

    } catch ( Exception $e ) {

        wc_add_notice( __( 'There was an error locating the switch details.', 'woocommerce-subscriptions' ), 'error' );
        WC()->cart->empty_cart( true );
        return array();
    }
}
add_filter( 'woocommerce_add_cart_item_data', 'kbhl_set_switch_details_in_cart', 11, 3 );





/**
 * Gets subscription data for current user.
 *
 * @return  array  Subscription data, also stored to global variable $kbhl_global_current_subscription_data
 */
function kbhl_get_current_subscription_data() {
    global $kbhl_adding_switch_to_cart, $kbhl_global_current_subscription_data;

    if ( ! $kbhl_adding_switch_to_cart ) {
        return array();
    }

    if ( ! empty( $kbhl_global_current_subscription_data ) ) {
        return $kbhl_global_current_subscription_data;
    }

    $subscription_data = array();
    $subs              = wcs_get_users_subscriptions();

    if ( ! empty( $subs ) ) {
        foreach ( $subs as $sub ) {
            $subscription_data['id'] = $sub->get_id();

            foreach ( $sub->get_items() as $item_id => $item ) {
                $subscription_data['item'] = $item_id;
                break; // There should only be 1 order item.
            }

            break; // There should only be 1 subscription.
        }
    }

    $kbhl_global_current_subscription_data = $subscription_data;

    return $kbhl_global_current_subscription_data;
}

With this added you can add the switch to the cart as follows:

global $kbhl_adding_switch_to_cart; // If run inside a function.
WC()->cart->empty_cart( true );
$kbhl_adding_switch_to_cart = true;
WC()->cart->add_to_cart( 1907, 1, 1927 );
$kbhl_adding_switch_to_cart = false; // Reset after to get back to default validation.
Punchlinern
  • 714
  • 5
  • 17
  • 33
  • 1
    Thanks for sharing your solution. – GrafiCode Aug 20 '21 at 11:15
  • 1
    @GrafiCode Thank you! I could add the the function at the end, `kbhl_get_current_subscription_data()`, might be specific for my case, as it assumes a user only has one subscription, and that a subscription only contains 1 item. – Punchlinern Aug 20 '21 at 12:29