Installment payments breaks down the full purchase price of items into smaller amounts over periods of time to make purchasing more reachable for high priced items. A recurring billing is used to capture these smaller payments. There are 2 existing WooCommerce plugins which can help achieve this behavior. The first is WooCommerce Deposits. The other is WooCommerce Subscriptions. There are shortcomings to both plugins which I will discuss below. Finally, I will describe a custom implementation which was deployed successfully on a customer site that overcomes these shortcomings as well as a bonus in providing dynamic calculations of installments. Developers can use this article as a guide to implement their own solutions.
Shortcomings of existing plugin solutions
WooCommerce Deposits provides installment payments behavior by allowing customers to choose a pre-defined payment plan for a specific product in WooCommerce. However, there are 3 shortcomings. First, the store admin must setup 1 or more payment schedules for each product that they wish to offer an installment payment plan for. For stores with many and varied price items, this can be a tedious chore. What if prices of items change? These payment schedules must change as well. Second, how would coupon discounts be supported for installment purchases if payment schedules and amounts must be pre-defined? Finally, the biggest issue of all is recurring billing relies on the customer manually logging into the system to make the payment as opposed to automatically billing the customer every period. With that said, WooThemes is working on automating the recurring billing but it is not available as of this writing. When automatic recurring billing becomes available, this plugin can be considered for installment payments as long as the other 2 limitations are understood.
WooCommerce Subscriptions is designed for subscription based selling. But it can be used to implement installments by allowing the user to purchase pre-defined billing schedules and the plugin will manage the recurring billing. The system can be customized to cancel the recurring billing, and thus the subscription once the total amount captured from recurring billing equals the full price of the item. This method of offering installment payments still has the problem of having the store admin create these pre-defined billing schedules. I am unsure if coupon discounts work, but the plugin says, “Delight customers by offering subscribers special discounts on their payments or sign-up fee”.
Installment payments custom implementation
The idea behind the custom implementation is to dynamically calculate the installment payment prices at run-time. The calculations can be based on product price, start date, or any type of logic. With the calculated installment payment price, adjust the cart / checkout in WooCommerce to reflect the installment price if the user selects purchase using installments. Since the installment price is determined at run-time. There will be full support for coupons. This will be accomplished using existing filters and hooks of the stock WooCommerce plugin. Following the normal WooCommerce checkout process, we will record all the future installments details in the database. Finally, an automatic cron job will be configured to run and process any recurring billing based on the installments database table. The rest of the article will describe in detail the custom code in these filters and hooks which are used to complete the WooCommerce side of the solution. In part 2 of this series, we will discuss the recurring billing and implementation.
Requirements
- WooCommerce plugin
- Advanced Custom Fields plugin
- API library of your Payment gateway (For recurring billing)
Hooks and filters used
To begin, we will start with defining the logic on how to break down the full purchase price into equal installments. Here is where the dynamic installment calculations takes place. The example logic used is based on number of months before program start date. Payment in full must be received by the store before the program start date. For example, if a summer camp program begins on August 1, 2016 and today is May 1, 2016. Then there would be 3 months / payments before the program start date. May, June, and July 1st.
Put this logic in the woocommerce_after_add_to_cart_button
action which is called on the product detail page after the Add to Cart button. This will clearly show to the customer each individual future payment in an easy to view table. In addition, the user can choose between purchasing the program in full by clicking on the Register in Full button or in installments using the new Register using Installments button.
add_action('woocommerce_after_add_to_cart_button', 'ttp_installment_table');
function ttp_installment_table() { global $product; $installment_data = ttp_installment_data($product->id); $installment_html = ttp_installment_html($installment_data); echo $installment_html; }
/** * Calculate installment price based on time to program start. */ function ttp_installment_data($program_id) { $installments = array(); /* Program start date is stored as a custom field using * the Advanced Custom Fields plugin */ $program_start_date_meta = get_post_meta($program_id, 'program_start_date', true); if (!empty($program_start_date_meta)) { $program_price = get_post_meta($program_id, '_regular_price', true); /* Calculate the number of months before the program * start date. There is a date overflow which needs to * be fixed. Check back for part 2 of this series on * how to fix the problem */ $today = new DateTime(NULL, new DateTimeZone("UTC")); $program_start_date = new DateTime(NULL, new DateTimeZone("UTC")); preg_match("/(\d{4})(\d{2})(\d{2})/", $program_start_date_meta, $results); list(, $year, $month, $day) = $results; $program_start_date->setDate($year, $month, $day); $date_diff = $today->diff($program_start_date, false); $periods = $date_diff->m; /* array where key = date and value = amount * E.g. array ('1/1/2015' => 800, '2/1/2015' => 1000); */ $installment_price = $program_price / $periods; for ($i=0;$i<$periods;$i++) { $installments[$today->format("m/d/y")] = $installment_price; $today->modify("+1 month"); } } return $installments; }
/** * Generate the HTML to display the installment payment * table and Register and pay using Installments button. */ function ttp_installment_html($installment_data) { /* Generate the installment payment table */ $installment_table = '<div class="inst-table"><table>'; $i = 1; foreach ($installment_data as $key => $value) { $installment_table .= '<tr><td class="installment-td">Payment ' . $i . '</td><td class="installment-td">'; $installment_table .= $key; $installment_table .= '</td><td class="installment-td">$' . $value; $installment_table .= '</td></tr>'; $i++; } $installment_table .= '</table></div>'; /* Additional Register and pay using Installments button */ $installment_html = '<div class="or">Or</div>'; $installment_html .= '<div class="small_button installment-button"><a class="button" href="#" onclick="installmentPurchase(); return false;">Register and pay with Installment Plan</a></div><div class="clearfix"></div>'; $installment_html .= $installment_table; $installment_html .= '*Register using Installments involves risks and a one-time 3% service fee. Review our <a href="">Installment Terms</a> for details.'; return $installment_html; }
Add this Javascript function to your site to flag a purchase as an installment purchase. This function will trigger when the user clicks on the new Register and pay with Installment Plan button.
function installmentPurchase() { jQuery(".cart").append('<input type="hidden" name="installment_purchase" value="yes" />'); jQuery(".cart").submit(); }
Here is where we will detect that the user clicked on the new Register and pay with Installment Plan button. The next step is to store the installment information into the cart item meta data. We will use the woocommerce_add_cart_item_data
filter which is called when add_to_cart is happening on the server side.
add_filter('woocommerce_add_cart_item_data', 'ttp_add_cart_item_meta', 10, 3);
/** * Add a piece of data to the cart item to flag it as an installment purchase */ function ttp_add_cart_item_meta($cart_item_data, $product_id, $variation_id) { if (isset($_REQUEST['installment_purchase']) && 'yes' == $_REQUEST['installment_purchase']) { $installments_data = ttp_installment_data($product_id); $cart_item_data['installment_purchase'] = $_REQUEST['installment_purchase']; $cart_item_data['installment_price'] = array_values($installments_data)[0]; $cart_item_data['installment_count'] = count($installments_data); $cart_item_data['installments'] = $installments_data; } return $cart_item_data; }
Next, we will adjust the price of the program in the cart to reflect the first installment. Add this to the woocommerce_before_calculate_totals
action. This action is called by WooCommerce during recalculation of the cart / checkout page grid before any coupons or taxes are applied.
add_action('woocommerce_before_calculate_totals', 'ttp_installment_adjustment', 10, 1);
function ttp_installment_adjustment($cart_object) { foreach ($cart_object->cart_contents as $key => $value) { if (isset($value['installment_purchase']) && 'yes' == $value['installment_purchase']) { $value['data']->set_price(floatval($value['installment_price'])); } } }
We will also need to store program meta data into the WooCommerce order meta data using woocommerce_add_order_item_meta
This action is called during the order creation after checkout completes. The meta is stored permanently in the database table prefix_woocommerce_order_itemmeta
for this order. It will also be used after checkout to keep track of future installments which needs to be billed.
add_action('woocommerce_add_order_item_meta', 'ttp_add_order_item_meta', 10, 3);
/** * WooCommerce in process of creating an order. Add order meta data for * use later post checkout. */ function ttp_add_order_item_meta($item_id, $values, $cart_item_key) { if (isset($values['installment_purchase']) && 'yes' == $values['installment_purchase']) { wc_add_order_item_meta($item_id, '_installment_purchase', $values['installment_purchase'], false); wc_add_order_item_meta($item_id, '_installment_price', $values['installment_price'], false); wc_add_order_item_meta($item_id, '_installment_count', $values['installment_count'], false); wc_add_order_item_meta($item_id, '_installment_payment_stream', $values['installments'], false); } }
As an optional step, you may want to change the program description in the cart show the user that the current payment is 1 of X payments. To change the program description in the cart, we will use the woocommerce_cart_item_name
filter.
add_filter('woocommerce_cart_item_name', 'ttp_cart_item_name_for_installments', 10, 3);
function ttp_cart_item_name_for_installments($title = null, $cart_item = null, $cart_item_key = null) { if (isset($cart_item['installment_purchase']) && 'yes' == $cart_item['installment_purchase']) { setlocale(LC_MONETARY, 'en_US'); $nInstallments = count($cart_item['installments']); $installmentPrice = round(floatval($cart_item['installment_price']), 2); echo sprintf("<p>%s</p><p><em>Installment 1 of %d</em></p><p><em>%d remaining installments of $%s</em></p>", $title, $nInstallments, $nInstallments - 1, money_format('%i', $installmentPrice)); } else { echo sprintf("</p>%s</p>", $title); } }
Installments in action
Regular payment in full and installment purchase is possible at the same time. Coupon processing is also supported. However, you may want to adjust the coupon across all the future installment payments as opposed to applying it once which is the default behavior.
If a customer registered on April 1, 2016 for the Youth camp summer experience which starts on August 1, 2016. The installment plan will be a 4 month installment plan as opposed to a 3 month installment plan in the example above.
At this point, the initial installment is complete and fully functioning. The customer can click on the program detail page to view the installment breakdown and choose to purchase using installments. The initial installment will be charged to the user. The last step is to capture the remaining installments using an automatic cron job. We will cover this in part 2 of this series.
Optional styling and UI changes
Change the Add to Cart button label using the woocommerce_product_single_add_to_cart_text
filter.
add_filter('woocommerce_product_single_add_to_cart_text', 'ttp_custom_cart_button_text');
function ttp_custom_cart_button_text() { return __( 'Register in Full', 'ttp' ); }
CSS
p.price, .woocommerce div.product span.price { color: #a46497; } .inst-table { overflow-x: auto; padding: 20px; clear:both; border-collapse: initial; } .inst-table table { width: 50%; } .inst-table th, td { padding: 2px; text-align: left; } tr:nth-child(even) {background-color: #ffffff} tr:nth-child(odd) {background-color: #dddddd} .or { display: block; clear: both; padding: 10px; width: 100px; text-align: center; font-weight: bold; } .installment-td { padding: 4px; text-align: center; } div.installment-button a.button { background: none repeat scroll 0 0 #a46497; font-size: 15px; text-decoration:none; float: left; color: #ffffff; } div.installment-button a.button:hover { color: #ffffff; background-color: #935386; }
I am available for consultation for WooCommerce installment payments custom implementation. Please navigate over to the contact tab to get in touch. Thank you!
If you have any questions or comments about this article. Please put them in the comments below.
Notes
- Part 2 of this series will discuss the recurring billing and implementation.
- This is only a guide. An implementation as a plugin would be ideal to separate installments logic from the rest of the theme.
- I am unaware of PayPal support for recurring billing as of June 21, 2016.
This is a great tutorial and exactly what I am looking for my site. Any estimate when you will be posting the part 2 of the tutorial sir. I am trying to use PayPal for gateway. Thanks again
I am unaware of any APIs from PayPal which would make recurring payment capture work. With PayPal, you have to go with the subscriptions method. The way PayPal works is it makes the user go to the PayPal website to process each individual transaction thereby maintaining PCI compliance. If you find anything otherwise, I would be interested in learning more about it.