How to Programatically Add a Custom Discount to WooCommerce Checkout

In this post we’ll look at how we add discounts programatically and automatically. To do this we utilize WooCommerce’s coupon functionality but we make it automatic, and we hide the fact that it’s a coupon to the customers. I have tried to keep the code as general as possible as the conditions for applying discounts can vary a lot for anyone reading this.

Why coupons are the way to do it

In older versions of WooCommerce we had a few (“hackish”) options to add discounts with code; for example providing a negative number to add_fee() or working with get_discounted_price(). Unfortunately these methods no longer work or are considered legacy and will be removed in later versions. The structurally good way to add discounts is using the built-in coupon functionality in WooCommerce.

However a coupon generally cannot be applied automatically to a cart. It requires the customer to manually type in a coupon code she or he must have been given. In some webshops you just want a discount applied automatically when a certain condition have been met without the customer having to do anything. That’s what we’ll fix using code.

Custom discounts with coupons

You will need to create a coupon with a code and set it up with the parameters you wish. You’ll be able to apply it, remove it, and change different outputs wherever the coupon is visible for the customer. Optionally, if you don’t actually want coupons available for customers in your webshop, we’ll look at some simple tricks to hide the options for manually entering coupon codes.

In the code example below I’m grouping all related code into a PHP class. This helps for cleaner code but is especially useful because we want to refer to the coupon code several times. Defining it as a class variable is better than repeating it in multiple functions.

Create your discount as a coupon

The first step is creating the coupon which will be the “placeholder” for your discount. The easiest is simply creating one in WooCommerce admin (WooCommerce > Coupons > Add Coupon).

If you want to create the coupon programatically, a coupon is actually a post you can create with wp_insert_post() – but you need to take care to create your coupon post only once. WooCommerce has a short guide in how to programatically create a coupon.

Provide an unique slug as your coupon code, and as minimum set up the discount amount (either a percentage or fixed fee). As for the code example below I have created a coupon code ‘example‘ (I recommend using a more creative and fitting name for yours though).

Creating the PHP class that will contain all our code

Let’s finally get to the coding! You can define this class directly inside your theme’s functions.php, plugin code, or a included separate file.

class AWPCustomDiscount {
	private $coupon_code = 'example';

	public function __construct() {

	}
}
new AWPCustomDiscount();

Name your class anything you’d like, and replace the class variable $coupon_code with whatever you named your coupon. Don’t forget to initialize the class after at the end (new AWPCustomDiscount()).

If you have programmed object-oriented before this should be familiar to you. If not, don’t worry, this should be simple to follow. We’ll add a few functions inside here and refer to the coupon code with $this->coupon_code. Let’s start with the code responsible for actually adding the coupon when conditions are met.

Applying coupon programatically

By experience I have learned that hooking the code for applying and removing coupons work best and most consistently using the following two hooks; one for the cart (woocommerce_before_cart) and one for the checkout (woocommerce_before_checkout_form).

Inside the function all we need to do is checking for the conditions we want. WooCommerce offers functions to get ahold of the cart object with WC()->cart which we can use to get all information we need about the cart such as its items and totals. As an example the code below will apply a discount if the cart’s total (excluding any discounts) are above a certain amount.

We should also ensure that if conditions are not met, we should remove the discount if it was previously added. In our example, imagine the customer exceeded the maximum amount, but then went to the cart, removed some items and thus getting back below the required amount. As coupons are not automatically applied, any applied coupons need to be programatically removed as well.

Using apply_coupon

Coupons are added to the cart object with apply_coupon() providing the coupon code as parameter, and they are removed from the cart object with remove_coupon() with the coupon code as parameter. Simple enough. We can also use the function so aptly named get_applied_coupons() in the cart object to check if our coupon has already been applied.

	...
	public function __construct() {
		add_action('woocommerce_before_cart', [$this, 'addDiscount']);
		add_action('woocommerce_before_checkout_form', [$this, 'addDiscount']);
	}

	function addDiscount() {
		if (is_admin() && !defined('DOING_AJAX')) {
			return;
		}
		
		if (WC()->cart->get_subtotal() > 500) {
			// add discount, if not added already
			if (!in_array($this->coupon_code, WC()->cart->get_applied_coupons())) {
				WC()->cart->apply_coupon($this->coupon_code);
			}
		} else {
			// remove discount if it was previously added
			WC()->cart->remove_coupon($this->coupon_code);
		}
	}
	...

If you are unfamiliar with using object-oriented PHP in WordPress; here’s a quick summary in how the above works: The __construct() function will run whenever the class is initialized, which we do right away after the class. Inside __construct() you’ll usually add all hooks as you’d normally do for example inside functions.php. In order for WordPress to find our functions inside our class we need to tell the hook to refer to a function defined in an array consisting of $this (the class object) and the function name.

Inside the function addDiscount() we program in our conditions. In the above example I fetch the cart’s total using WC()->cart->get_subtotal() and compares it to my conditions – being above 500.

Note: If you need to fetch and compare cart totals, be aware that most of the totals are included coupons. For example the more familiar WC()->cart->get_cart_contents_total() will return the total before shipping but included discounts; which means if your discount has previously been applied, this total will be wrong to compare against.

You can call apply_coupon() without checking if the coupon already has been applied (the if check at line #14), because the function apply_coupon() will itself make sure to not re-add it if it was applied before. However this if-check prevents the customer getting a error message saying “The coupon has already been applied” every time the cart is updated.

This is how your discount will appear in cart totals in cart and checkout:

Your conditions for discount

The conditions for your coupon depends entirely up to you and your needs. I have set up a few example conditions for adding a custom fee in an earlier post – for example depending on shipping location or which products are in cart. You can also compare today’s date if you wanted to give a special Christmas or Halloween discount, or give a discount based on conditions at the logged-in customer.

If you rather want to apply the coupon depending on settings you’ve set on the coupon itself – for example exclude or include certain product IDs, you can do so as well. You can instantiate an object of the coupon from the coupon code, like so:

$coupon = new WC_Coupon($this->coupon_code);

With that coupon object you have access to all functions for fetching any settings you’ve set on the coupon. For example getting ahold of product IDs that must be included for the coupon:

$required_products = $coupon->get_product_ids();

Refer to the documentation for the class WC_Coupon to see how you can get ahold of what you need.

Fix the coupon’s visible label

The coupon should by now be added and removed when your cart meets or fails to meet your conditions set in addDiscount(). However in the totals table in both cart and in checkout the discount is displayed as “Coupon: example” (or whatever your coupon code is). That’s not good! Luckily it’s easy to change this label by using the filter woocommerce_cart_totals_coupon_label. We’ll add the filter to our __construct() and define a function:

	...
	public function __construct() {
		add_action('woocommerce_before_cart', [$this, 'addDiscount']);
		add_action('woocommerce_before_checkout_form', [$this, 'addDiscount']);
		add_filter('woocommerce_cart_totals_coupon_label', [$this, 'discountLabel'], 10, 2);
	}

	function discountLabel($label, $coupon) {
		if ($coupon->code == $this->coupon_code) {
			return __('Custom discount', 'txtdomain');
		}
		return $label;
	}
	...

With the filter we get access to the coupon object as second parameter. With the coupon object we can check its property, code, whether or not it’s a match to our custom coupon code. If so we return whatever label we want. Adjust the label output to whatever you want. Otherwise we return the default coupon label.

Remove the user option to remove the discount

Another thing you might have noticed in the display of our discount in cart and checkout is that it got a link “[Remove]” after its amount. This is default behaviour of coupons in WooCommerce, but for our case not desirable. Both because it might confuse the customers and because our code would re-apply it after removal.

WooCommerce provides the filter woocommerce_cart_totals_coupon_html for the output of the amount. Luckily WooCommerce provides the HTML output of only the amount itself as third parameter to this filter. We can simply return this instead of making sure our discount is presented correctly with currency and everything.

	...
	public function __construct() {
		add_action('woocommerce_before_cart', [$this, 'addDiscount']);
		add_action('woocommerce_before_checkout_form', [$this, 'addDiscount']);
		add_filter('woocommerce_cart_totals_coupon_label', [$this, 'discountLabel'], 10, 2);
		add_filter('woocommerce_cart_totals_coupon_html', [$this, 'discountHtml'], 10, 3);
	}

	function discountHtml($coupon_html, $coupon, $discount_amount_html) {
		if ($coupon->code == $this->coupon_code) {
			return $discount_amount_html;
		}
		return $coupon_html;
	}
	...

Optional: Hiding the coupon functionality for customers

Coupons must be activated in WooCommerce for our discount functionality to work, but be aware that this will add the option for customers manually entering coupon codes in both cart and checkout. If you don’t want to show that your webshop has coupon functionality, and especially disallow customers to manually enter coupon codes, you can do so too.

Removing it from checkout

Removing the coupon functionality from checkout is easy and can be done inside our class. We simply remove the hook that renders the coupon “box”, like so:

	...
	public function __construct() {
		add_action('woocommerce_before_cart', [$this, 'addDiscount']);
		add_action('woocommerce_before_checkout_form', [$this, 'addDiscount']);
		add_filter('woocommerce_cart_totals_coupon_label', [$this, 'discountLabel'], 10, 2);
		add_filter('woocommerce_cart_totals_coupon_html', [$this, 'discountHtml'], 10, 3);
		remove_action('woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form', 10);
	}
	...

Removing it from cart

Removing the coupon code functionality in cart is unfortunately not as easy as this is hardcoded in a template. You will need to override WooCommerce’s cart template to remove the coupon functionality. Copy the file plugins\woocommerce\templates\cart\cart.php into your-theme\woocommerce\cart\cart.php.

WooCommerce’s template files are constantly changing so giving you an exact line number wouldn’t help. But you’ll find the code that outputs the input and button for coupon close to the bottom of the file; looking somewhat like this:

cart.php
...
<?php if ( wc_coupons_enabled() ) { ?>
	<div class="coupon">
		<label for="coupon_code"><?php esc_html_e( 'Coupon:', 'woocommerce' ); ?></label> <input type="text" name="coupon_code" class="input-text" id="coupon_code" value="" placeholder="<?php esc_attr_e( 'Coupon code', 'woocommerce' ); ?>" /> <button type="submit" class="button" name="apply_coupon" value="<?php esc_attr_e( 'Apply coupon', 'woocommerce' ); ?>"><?php esc_attr_e( 'Apply coupon', 'woocommerce' ); ?></button>
		<?php do_action( 'woocommerce_cart_coupon' ); ?>
	</div>
<?php } ?>
...

What you need to do is comment out the coupon-related output. I recommend adding a PHP comment with /* right before the if-check and closing it with */ right after the closing bracket. Like so:

cart.php
...
<?php /* if ( wc_coupons_enabled() ) { ?>
	<div class="coupon">
		<label for="coupon_code"><?php esc_html_e( 'Coupon:', 'woocommerce' ); ?></label> <input type="text" name="coupon_code" class="input-text" id="coupon_code" value="" placeholder="<?php esc_attr_e( 'Coupon code', 'woocommerce' ); ?>" /> <button type="submit" class="button" name="apply_coupon" value="<?php esc_attr_e( 'Apply coupon', 'woocommerce' ); ?>"><?php esc_attr_e( 'Apply coupon', 'woocommerce' ); ?></button>
		<?php do_action( 'woocommerce_cart_coupon' ); ?>
	</div>
<?php } */ ?>
...

At this point your webshop’s coupon functionality should be virtually hidden for customers! But your custom discount should work perfectly fine.

Leave a comment