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.