Automatically apply a coupon to logged in users

Force apply a permanent coupon to logged-in WooCommerce users to provide a client discount.

Skip to final code

One way to provide a discount to logged in users would be to dynamically modify the product prices, how ever this method doesn’t show as a discount in a WC Order. Here is a method that applies the discount as a coupon that is automatically applied to the Cart when the customer is logged in.

First let’s create a coupon that handles the discount from Marketing -> Coupons -> Add coupon.

This unlimited coupon provides a 10% discount.

Now let’s apply this coupon automatically when the user is logged in by utilizing the woocommerce_check_cart_items action (and also remove the coupon when the user is not logged in).

Handle couponPHP
add_action('woocommerce_check_cart_items', function() {

	$cart = WC()->cart;
	$coupons = $cart->get_applied_coupons();
	$coupon_code = strtolower('CLIENT-DISCOUNT');
	$has_coupon_applied = in_array($coupon_code, $coupons, true);
	$should_apply_coupon = is_user_logged_in() && $cart->get_cart_contents_count() > 0;

	if($should_apply_coupon && !$has_coupon_applied) {
		$cart->set_applied_coupons(array_merge($coupons, [$coupon_code]));
	} elseif(!$should_apply_coupon && $has_coupon_applied) {
		$cart->set_applied_coupons(array_filter($coupons, function($code) use ($coupon_code) {
			return $code !== $coupon_code;
		}));
	}
});

While this code already gets the job done, there are a couple of shortcomings:

  • Coupon might get deleted and no longer exist.
  • The coupon can be removed in Checkout.
  • The coupon has a [Remove] link in Cart as well as Checkout.

Before tackling these issues, it’s best to make the code modular and reusable by using a PHP class.

Modular version of the codePHP
class Ska_Forced_Coupon {

	/** @var string */
	private $code = '';

	/** @var Callable */
	private $condition = '__return_false';

	/**
	 * @param string $code Coupon code to use.
	 * @param Callable $condition Determines if the coupon should be applied.
	 */
	public function __construct($code, $condition) {
		$this->code = strtolower($code);
		$this->condition = $condition;
	}

	/**
	 * Registers functionality through WordPress hooks.
	 */
	public function register() {
		add_action('woocommerce_check_cart_items', [$this, 'woocommerce_check_cart_items']);
	}

	/**
	 * @return WC_Cart
	 */
	protected function get_cart() {
		return WC()->cart;
	}

	/**
	 * @return string
	 */
	protected function get_code() {
		return $this->code;
	}

	/**
	 * @return bool
	 */
	protected function has_coupon_applied() {
		$coupons = $this->get_cart()->get_applied_coupons();
		return in_array($this->get_code(), $coupons, true);
	}

	/**
	 * @return bool
	 */
	protected function should_apply_coupon() {
		$callback = $this->condition;
		return (bool)$callback() && $this->get_cart()->get_cart_contents_count() > 0;
	}

	protected function apply_coupon() {
		$cart = $this->get_cart();
		$coupons = $cart->get_applied_coupons();
		$cart->set_applied_coupons(array_merge($coupons, [$this->get_code()]));
	}

	protected function remove_coupon() {
		$cart = $this->get_cart();
		$coupons = $cart->get_applied_coupons();
		$cart->set_applied_coupons(array_filter($coupons, function($code) {
			return $code !== $this->get_code();
		}));
	}

	public function woocommerce_check_cart_items() {

		$has_coupon_applied = $this->has_coupon_applied();
		$should_apply_coupon = $this->should_apply_coupon();

		if($should_apply_coupon && !$has_coupon_applied) {
			$this->apply_coupon();
		} elseif(!$should_apply_coupon && $has_coupon_applied) {
			$this->remove_coupon();
		}
	}
}

add_action('woocommerce_init', function() {
	$client_discount = new Ska_Forced_Coupon('CLIENT-DISCOUNT', 'is_user_logged_in');
	$client_discount->register();
});

Now that the code is structured like that it’s much easier to add additional logic to handle other issues as well as add other automatic coupons with different conditions.

Using the woocommerce_init action instead of init also ensures that this code only runs when WooCommerce is active and won’t trigger fatal errors when it is temporarily disabled.

Verifying coupon exists

If the coupon were to be removed the code would be trying to automatically apply a non-existent coupon. Placing a check before adding any hooks prevents it.

// Updated register() function:

/**
 * Registers functionality through WordPress hooks.
 */
public function register() {

	if(!$this->coupon_exists()) {
		return;
	}

	add_action('woocommerce_check_cart_items', [$this, 'woocommerce_check_cart_items']);
}

// Added to class:

/**
 * @return bool
 */
protected function coupon_exists() {
	$coupon = new WC_Coupon($this->get_code());
	return $coupon->get_id() && $coupon->get_status() !== 'trash';
}
PHP

The coupon exists in the database when it has an ID.

Preventing remove

Under WooCommerce checkout the coupon has a “remove” link:

The coupon is removed via an AJAX action and the logic re-applying it wouldn’t reflect on the checkout page without a reload. How ever, since the field $_POST['coupon'] is used to remove it we can step in and unset it before the removal happens:

// Added to register():
add_action('wc_ajax_remove_coupon', [$this, 'prevent_ajax_remove'], 5);

// Added to class:
public function prevent_ajax_remove() {
	if(
		isset($_POST['coupon']) 
		&& check_ajax_referer('remove-coupon', 'security', false)
		&& strtolower($_POST['coupon']) === $this->get_code() // phpcs:ignore
		&& $this->should_apply_coupon()
	) {
		unset($_POST['coupon']);
	}
}
PHP

Now, when trying to remove the coupon from checkout it should respond with the “Sorry there was a problem removing this coupon” message as it didn’t find a coupon to remove.

Hiding remove

Since the coupon can’t be removed we can also disable the visibility of the “Remove” links themselves:

// Added to register():
add_action('wp_enqueue_scripts', [$this, 'hide_remove_links']);

// Added to class:
public function hide_remove_links() {
	wp_add_inline_style(
		'woocommerce-inline',
		sprintf(
			'a.woocommerce-remove-coupon[data-coupon="%s"] { display: none !important; }',
			esc_attr($this->get_code())
		)
	);
}
PHP

Final code

For maximum cleanliness, let’s also extract the logic class into a separate file in your child theme’s folder.

/wp-content/themes/your-child-theme/ska-forced-coupon.phpPHP
<?php

defined('ABSPATH') || exit;

class Ska_Forced_Coupon {

	/** @var string */
	private $code = '';

	/** @var Callable */
	private $condition = '__return_false';

	/**
	 * @param string $code Coupon code to use.
	 * @param Callable $condition Determines if the coupon should be applied.
	 */
	public function __construct($code, $condition) {
		$this->code = strtolower($code);
		$this->condition = $condition;
	}

	/**
	 * Registers functionality through WordPress hooks.
	 */
	public function register() {

		if(!$this->coupon_exists()) {
			return;
		}

		add_action('woocommerce_check_cart_items', [$this, 'woocommerce_check_cart_items']);
		add_action('wc_ajax_remove_coupon', [$this, 'prevent_ajax_remove'], 5);
		add_action('wp_enqueue_scripts', [$this, 'hide_remove_links']);
	}

	/**
	 * @return WC_Cart
	 */
	protected function get_cart() {
		return WC()->cart;
	}

	/**
	 * @return string
	 */
	protected function get_code() {
		return $this->code;
	}

	/**
	 * @return bool
	 */
	protected function coupon_exists() {
		$coupon = new WC_Coupon($this->get_code());
		return $coupon->get_id() && $coupon->get_status() !== 'trash';
	}

	/**
	 * @return bool
	 */
	protected function has_coupon_applied() {
		$coupons = $this->get_cart()->get_applied_coupons();
		return in_array($this->get_code(), $coupons, true);
	}

	/**
	 * @return bool
	 */
	protected function should_apply_coupon() {
		$callback = $this->condition;
		return (bool)$callback() && $this->get_cart()->get_cart_contents_count() > 0;
	}

	protected function apply_coupon() {
		$cart = $this->get_cart();
		$coupons = $cart->get_applied_coupons();
		$cart->set_applied_coupons(array_merge($coupons, [$this->get_code()]));
	}

	protected function remove_coupon() {
		$cart = $this->get_cart();
		$coupons = $cart->get_applied_coupons();
		$cart->set_applied_coupons(array_filter($coupons, function($code) {
			return $code !== $this->get_code();
		}));
	}

	public function woocommerce_check_cart_items() {

		$has_coupon_applied = $this->has_coupon_applied();
		$should_apply_coupon = $this->should_apply_coupon();

		if($should_apply_coupon && !$has_coupon_applied) {
			$this->apply_coupon();
		} elseif(!$should_apply_coupon && $has_coupon_applied) {
			$this->remove_coupon();
		}
	}

	public function prevent_ajax_remove() {
		if(
			isset($_POST['coupon'])
			&& check_ajax_referer('remove-coupon', 'security', false)
			&& strtolower($_POST['coupon']) === $this->get_code() // phpcs:ignore
			&& $this->should_apply_coupon()
		) {
			unset($_POST['coupon']);
		}
	}

	public function hide_remove_links() {
		wp_add_inline_style(
			'woocommerce-inline',
			sprintf(
				'a.woocommerce-remove-coupon[data-coupon="%s"] { display: none !important; }',
				esc_attr($this->get_code())
			)
		);
	}
}

In your child theme’s functions.php include the file and initialize any coupons:

/wp-content/themes/your-child-theme/functions.phpPHP
require_once get_stylesheet_directory() . '/ska-forced-coupon.php';

add_action('woocommerce_init', function() {
	$client_discount = new Ska_Forced_Coupon('CLIENT-DISCOUNT', 'is_user_logged_in');
	$client_discount->register();
});

Other coupons and conditions

Since the coupon condition accepts a callback we can add different coupons based on different dynamic conditions, such as checking the cart contents or user status.

VIP discountPHP
$vip_discount = new Ska_Forced_Coupon(
	'VIP-DISCOUNT', 
	function() {
		return (
			is_user_logged_in() 
			&& (bool)get_user_meta(get_current_user_id(), 'ska-is-vip-user', true)
		);
	}
);
$vip_discount->register();

Leave a Reply

Your email address will not be published. Required fields are marked *