Skip to content

Instantly share code, notes, and snippets.

@xlplugins
Created August 31, 2026 14:43
Show Gist options
  • Select an option

  • Save xlplugins/e103b1a60e55b20b01f89aac27aeabbd to your computer and use it in GitHub Desktop.

Select an option

Save xlplugins/e103b1a60e55b20b01f89aac27aeabbd to your computer and use it in GitHub Desktop.
merge upsell subscription into one
<?php
/**
* Runs after FunnelKit has created the upsell subscription (priority 99, so the
* gateway handlers that store the payment token on it have already run).
*
* @param WC_Subscription $new_subscription Subscription FunnelKit just created.
* @param string $hash Offer product hash (unused).
* @param WC_Order $subscription_order Order the upsell was recorded against.
*/
add_action( 'wfocu_subscription_created_for_upsell', function ( $new_subscription, $hash, $subscription_order ) {
if ( ! function_exists( 'wcs_get_subscriptions_for_order' ) || ! $new_subscription instanceof WC_Subscription ) {
return;
}
$log = function ( $message ) {
if ( function_exists( 'WFOCU_Core' ) && ! is_null( WFOCU_Core()->log ) ) {
WFOCU_Core()->log->log( '[merge-subscription] ' . $message );
}
};
// The upsell may have been recorded against a NEW order. The parent
// subscription belongs to the ORIGINAL order, so ask FunnelKit for it and
// only fall back to the order we were handed.
$parent_order = null;
if ( function_exists( 'WFOCU_Core' ) && ! is_null( WFOCU_Core()->data ) ) {
$parent_order = WFOCU_Core()->data->get_parent_order();
}
if ( ! $parent_order instanceof WC_Order ) {
$parent_order = $subscription_order;
}
if ( ! $parent_order instanceof WC_Order ) {
$log( 'no parent order, leaving the new subscription alone' );
return;
}
$parent_subscriptions = wcs_get_subscriptions_for_order( $parent_order->get_id(), array( 'order_type' => 'parent' ) );
// Drop the one we have just been given, in case it is linked to this order.
foreach ( $parent_subscriptions as $key => $candidate ) {
if ( $candidate->get_id() === $new_subscription->get_id() ) {
unset( $parent_subscriptions[ $key ] );
}
}
if ( 1 !== count( $parent_subscriptions ) ) {
$log( sprintf( 'expected exactly 1 parent subscription, found %d - leaving both in place', count( $parent_subscriptions ) ) );
return;
}
$parent_subscription = array_pop( $parent_subscriptions );
// Two subscriptions can only share a schedule if they bill on the same one.
if (
$parent_subscription->get_billing_period() !== $new_subscription->get_billing_period()
|| (int) $parent_subscription->get_billing_interval() !== (int) $new_subscription->get_billing_interval()
) {
$log( sprintf(
'billing schedule differs (parent %s/%s vs upsell %s/%s) - cannot merge',
$parent_subscription->get_billing_interval(),
$parent_subscription->get_billing_period(),
$new_subscription->get_billing_interval(),
$new_subscription->get_billing_period()
) );
return;
}
if ( ! $parent_subscription->has_status( array( 'active', 'pending', 'on-hold' ) ) ) {
$log( 'parent subscription is ' . $parent_subscription->get_status() . ' - not merging into it' );
return;
}
try {
$moved = 0;
$copied_items = array();
foreach ( $new_subscription->get_items() as $item ) {
$copied_items[] = $item->get_id();
$copy = new WC_Order_Item_Product();
$copy->set_props( array(
'product_id' => $item->get_product_id(),
'variation_id' => $item->get_variation_id(),
'quantity' => $item->get_quantity(),
'subtotal' => $item->get_subtotal(),
'subtotal_tax' => $item->get_subtotal_tax(),
'total' => $item->get_total(),
'total_tax' => $item->get_total_tax(),
'taxes' => $item->get_taxes(),
'name' => $item->get_name(),
) );
foreach ( $item->get_meta_data() as $meta ) {
$copy->add_meta_data( $meta->key, $meta->value, true );
}
$copy->add_meta_data( '_wfocu_merged_upsell', 'yes', true );
$parent_subscription->add_item( $copy );
$moved ++;
}
if ( 0 === $moved ) {
$log( 'upsell subscription had no line items' );
return;
}
$parent_subscription->calculate_totals( wc_tax_enabled() );
$parent_subscription->add_order_note( sprintf(
/* translators: %d: upsell subscription id */
__( 'Upsell product merged in from subscription #%d (FunnelKit one-click upsell).', 'funnel-builder' ),
$new_subscription->get_id()
) );
$parent_subscription->save();
/**
* Return false to keep the duplicate subscription as "cancelled" instead
* of deleting it. Deleting keeps the customer's account page clean.
*/
if ( apply_filters( 'wfocu_delete_merged_upsell_subscription', true, $new_subscription, $parent_subscription ) ) {
/*
* Delete the line items explicitly first. WC_Subscription::delete()
* clears wp_wc_orders / wp_posts / wp_wc_orders_meta but leaves the
* rows in wp_woocommerce_order_items and _itemmeta behind, and those
* orphans keep the deleted subscription visible in admin lists.
*/
foreach ( $copied_items as $item_id ) {
wc_delete_order_item( $item_id );
}
$new_subscription->delete( true );
} else {
// Keeping it as an audit trail - cancelled subscriptions do not bill,
// so the items can stay on it.
$new_subscription->update_status( 'cancelled', __( 'Merged into the parent subscription.', 'funnel-builder' ) );
}
$log( sprintf( 'merged %d item(s) into subscription #%d', $moved, $parent_subscription->get_id() ) );
} catch ( Exception $e ) {
$log( 'merge failed: ' . $e->getMessage() );
}
}, 99, 3 );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment