Skip to content

Instantly share code, notes, and snippets.

@devinsays
Last active February 7, 2021 15:08
Show Gist options
  • Save devinsays/c578d464e6f799738315136256b0795e to your computer and use it in GitHub Desktop.
Save devinsays/c578d464e6f799738315136256b0795e to your computer and use it in GitHub Desktop.
Creates a subscription in the background when a trial product is purchased.
<?php
/**
* When a trial product is purchased we create an order for a subscription product.
*
* This subscription order charges after 14 days if customer does not cancel.
*/
class Nano_Trial_Subscription {
/**
* The ID for our WooCommerce trial product.
* (Trial Kit)
*
* @since 2.2.0
* @access public
* @var int
*/
public $trial_product_id = 9344;
/**
* The ID for the subscription product.
* (Home Bundle)
*
* @since 2.2.0
* @access public
* @var int
*/
public $subscription_product_id = 6247;
/**
* The URL for the subscription product.
* (Home Bundle)
*
* @since 2.2.0
* @access public
* @var string
*/
public $subscription_product_url = '/product/for-the-home-bundle/';
/**
* Init the class.
*
* @since 2.2.0
*/
public function init() {
// The trial product requires additional cart and checkout rules.
add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'trial_product_cart_filtering' ), 100, 2 );
// Filters the add to cart message if the trial product is added.
add_filter( 'wc_add_to_cart_message_html', array( $this, 'trial_add_to_cart_message_html' ), 100, 2 );
// Adds an "agree to subscription" checkbox to the checkout page.
add_action( 'woocommerce_review_order_before_submit', array( $this, 'trial_agree_to_subscription_checkbox' ) );
// Saves the value of the "agree to subscription" checkbox to order meta.
add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'save_agree_to_subscription_meta' ), 100, 2 );
// Creates the pending subscription if trial product purchase was completed.
add_action( 'woocommerce_payment_complete', array( $this, 'check_payment_for_trial_product' ), 100, 1 );
// Action hook for creating the subscription.
add_action( 'nano_create_subscription_order', array( $this, 'create_subscription_order' ) );
}
/**
* Helper function to check if cart contains a specific $product_id.
*
* @since 2.2.0
* @param int $product_id
* @return boolean
*/
function nano_cart_contains( $product_id ) {
$products = WC()->cart->get_cart_contents();
foreach ( $products as $product => $values ) {
$cart_product_id = $values['data']->get_id();
if ( $product_id === $cart_product_id ) {
return true;
}
}
return false;
}
/**
* Restricts which items can be added to cart:
*
* 1) The $trial_product should not be added to cart if existing items are in the cart.
* 2) Other products should not be added to cart if $trial_product is already in cart.
*
* @since 2.2.0
* @param boolean $passed_validation
* @param int $product_id
* @return boolean
*/
function trial_product_cart_filtering( $passed_validation, $product_id ) {
// If cart is empty, no need for additional validation.
if ( WC()->cart->is_empty() ) {
return $passed_validation;
}
// If trial product is being added to cart, existing products should be removed.
// In product details, make sure "Enable this to only allow one of this item to be bought in a single order" is also checked.
if ( $this->trial_product_id === $product_id ) {
$notice = sprintf(
// Translators: Placeholder is for cart url.
__( 'Trial Product can not be purchased with other products. Please empty your <a href="%s">existing cart</a>.' ),
esc_url( wc_get_cart_url() )
);
wc_add_notice( $notice, 'notice' );
return false;
}
// If trial product is in cart, additional products should not be added.
if ( $this->nano_cart_contains( $this->trial_product_id ) ) {
$notice = sprintf(
// Translators: Placeholder is for cart url.
__( 'The Trial Product can not be ordered with other items.<br> Please remove the Trial Product from <a href="%s">your cart</a> if you wish to make other purchases.' ),
esc_url( wc_get_cart_url() )
);
wc_add_notice( $notice, 'notice' );
return false;
}
// Trial product is not in cart or being added.
return $passed_validation;
}
/**
* Strips out the "Shop" link from add_to_cart message if trial product is added.
*
* @since 2.2.0
* @param string $message
* @param array $products
* @return string
*/
public function trial_add_to_cart_message_html( $message, $products ) {
// ID of the trial product
$trial_product_id = 9344;
// Check cart for trial product
foreach ( $products as $product_id => $qty ) {
if ( $this->trial_product_id === $product_id ) {
// Strips out the "Shop" link/button from the message.
$message = preg_replace( '/<a href=\"(.*?)\">(.*?)<\/a>/', '', $message );
// Adds an additional notice about the subscription product
$notice = sprintf(
__( 'After 14 days this order will convert into a subscription for the <a href="%s">Home Bundle</a>. You will be reminded before renewal and can cancel at any time.' ),
esc_url( home_url( $this->subscription_product_url ) )
);
return $message . '<br><br>' . $notice;
}
}
// No changes needed, return default $message.
return $message;
}
/**
* Adds an "agree to subscription" checkbox on checkout.
*
* @since 2.2.0
* @return void
*/
public function trial_agree_to_subscription_checkbox() {
if ( $this->nano_cart_contains( $this->trial_product_id ) ) : ?>
<p class="form-row woocommerce-validated">
<?php /* Hidden input ensures a value will be posted even if visible input is unchecked. */ ?>
<input type="hidden" name="trial_subscription_accept" value="0">
<label>
<input id="trial-subscription-accept" name="trial_subscription_accept" type="checkbox" value="1" checked>
<span>I'd like this order to convert into a <a href="<?php echo esc_url( home_url( $this->subscription_product_url ) ); ?> ">Home Bundle</a> subscription after 14 days unless I cancel.</span>
</label>
</p>
<?php endif;
}
/**
* Saves the meta value of "agree to subscription" checkbox on checkout.
*
* @since 2.2.0
* @return void
*/
public function save_agree_to_subscription_meta( $order_id, $data ) {
$order = wc_get_order( $order_id );
// The "trial-subscription-accept variable" should only exist on orders with the trial product.
if ( isset( $_POST['trial_subscription_accept'] ) ) :
if ( intval( $_POST['trial_subscription_accept'] ) ) {
$order->update_meta_data( 'trial_subscription_accept', 1 );
} else {
$order->update_meta_data( 'trial_subscription_accept', 0 );
}
$order->save();
endif;
}
/**
* Runs after a payment completes (i.e. complete checkout + payment processed).
* If the order contains the trial product, we'll create the subscription order.
*
* @since 2.2.0
* @param int $order_id
* @return void
*/
public function check_payment_for_trial_product( $order_id ) {
// Get the order that was just placed.
$order = wc_get_order( $order_id );
// Meta data for subscription approval should only be on orders that contain the trial product.
$trial_subscription_accept = $order->get_meta( 'trial_subscription_accept' );
if ( 1 === intval( $trial_subscription_accept ) ) {
error_log( 'Scheduling subscription order.' );
$this->create_subscription_order( $order_id );
/*
* Ideally create_subscription_order would be scheduled.
* Current code throws a notice from Stripe extension.
$timestamp = time() + 60; // Generates timestamp 60 seconds in the future
wc_schedule_single_action( $timestamp, 'nano_create_subscription_order', array( $order_id ) );
*/
}
}
/**
* Create a subscription order with data from the completed trial product order.
*
* @since 2.2.0
* @param int $order_id
* @return void
*/
public function create_subscription_order( $trial_order_id ) {
error_log( 'Creating subscription order.' );
// Get order data from the original "Trial Kit" purchase.
$trial_order = wc_get_order( $trial_order_id );
$trial_order_data = $trial_order->get_data();
// All customers should have a user account automatically created.
$user_id = $trial_order->get_user_id();
// Creates the new order object.
$order = wc_create_order(
array(
'customer_id' => $user_id,
'payment_method' => $trial_order_data['payment_method'],
'payment_method_title' => $trial_order_data['payment_method_title'],
)
);
// Adds the product to the new order.
$product = wc_get_product( $this->subscription_product_id );
$quantity = 1;
$args = array();
// Sets the address.
$order->add_product( $product, $quantity, $args );
$order->set_address( $trial_order_data['billing'], 'billing' );
$order->set_address( $trial_order_data['shipping'], 'shipping' );
// Order totals are not calculated ($order->calculate_totals).
// This makes it so the first order is for 0.00.
// Get Stripe payment data.
// @TODO Can this be set in data store using CRUD methods?
$stripe_customer_id = get_post_meta( $trial_order_id, '_stripe_customer_id', true );
$stripe_card_id = get_post_meta( $trial_order_id, '_stripe_card_id', true );
// Add Stripe data to new order.
if ( $stripe_customer_id ) {
$order->update_meta_data( '_stripe_customer_id', $stripe_customer_id );
}
if ( $stripe_card_id ) {
$order->update_meta_data( '_stripe_card_id', $stripe_card_id );
}
$stripe = new WC_Gateway_Stripe();
$stripe->process_payment( $order->get_id() );
// Adds the subscription data to the new order.
$period = WC_Subscriptions_Product::get_period( $product );
$interval = WC_Subscriptions_Product::get_interval( $product );
// Create the subscription.
$subscription = wcs_create_subscription(
array(
'order_id' => $order->get_id(),
'billing_period' => $period,
'billing_interval' => $interval,
)
);
// Set trial_end date.
$default_start_date = wcs_get_datetime_utc_string( wcs_get_objects_property( $order, 'date_created' ) );
$start_date_time = wcs_date_to_time( $default_start_date ); // Convert to timestamp.
$trial_end = wcs_add_time( 14, 'days', $start_date_time ); // Add 14 days to $start_date_time.
$trial_end = date( 'Y-m-d H:i:s', $trial_end ); // Convert to date format.
$subscription->update_dates( array( 'trial_end' => $trial_end ) );
// Add the product.
$subscription->add_product( $product, $quantity, $args );
$subscription->calculate_totals();
// Activate the subscription.
WC_Subscriptions_Manager::activate_subscriptions_for_order( $order );
}
}
// Let's go!
$nano_trial_subscription = new Nano_Trial_Subscription();
$nano_trial_subscription->init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment