WooCommerce discount for logged-in users

An example of custom code that provides a discount for all logged-in users.

The code should ideally be placed in a file in your child theme’s directory and you can then include it from your child theme’s functions.php.
If you’re not using a child theme you can use mu-plugins instead.

Let’s start off with a simple class that can house the logic for the discounts:

PHP
<?php

final class SKA_Logged_In_Discount {

	/**
	 * Discount - amount to multiply original price with.
	 *
	 * @var float
	 */
	protected $discount = 0.9; // 10%

	public function __construct() {

		if(!class_exists('WooCommerce')) {
			return;
		}

		$this->register();
	}

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

add_action('init', function() {
	new SKA_Logged_In_Discount();
});

And here’s how you would include the file in your child theme’s functions.php:

PHP
require_once get_stylesheet_directory() . '/logged-in-discount.php';

When WooCommerce is not active the code is short-circuited as using any WC-related functions without the plugin active would cause fatal errors. This ensures the site won’t crash when WC plugin is disabled for whatever reason.

Next, let’s add a function that determines if a discount should be applied:

PHP
/**
 * Should given product be discounted.
 *
 * @param WC_Product $product
 * @return boolean
 */
public function discount_applies(WC_Product $product) {
	return (
		is_user_logged_in()
		&& !$product->is_on_sale()
	);
}

This function can simultaneously check whether the users’ current state (is_user_logged_in) is eligible for the discount and check the individual product as well. In this case we don’t want the product to receive a discount when it’s already on sale.

The following functions are used to apply the discount:

PHP
/**
 * Get product price with discount.
 *
 * @param string $price
 * @return string
 */
public function get_discounted_price($price) {

	if(!is_numeric($price)) {
		return $price;
	}

	return $price * $this->discount;
}

/**
 * Apply discount to product price.
 *
 * @param string $price Current price.
 * @param WC_Product $product
 * @return string Maybe discounted price.
 */
public function apply_discount($price, WC_Product $product) {

	if($this->discount_applies($product)) {
		return $this->get_discounted_price($price);
	}

	return $price;
}

Finally, here are some functions that actually connect (and disconnect) the discounts with WooCommerce:

PHP
public function enable_discount() {
	add_filter('woocommerce_product_get_price', [$this, 'apply_discount'], 10, 2);
	add_filter('woocommerce_product_get_regular_price', [$this, 'apply_discount'], 10, 2);
	add_filter('woocommerce_product_variation_get_price', [$this, 'apply_discount'], 10, 2);
	add_filter('woocommerce_get_variation_price', [$this, 'apply_discount'], 10, 2);
}

public function disable_discount() {
	remove_filter('woocommerce_product_get_price', [$this, 'apply_discount']);
	remove_filter('woocommerce_product_get_regular_price', [$this, 'apply_discount']);
	remove_filter('woocommerce_product_variation_get_price', [$this, 'apply_discount']);
	remove_filter('woocommerce_get_variation_price', [$this, 'apply_discount']);
}

Now all that’s left to do is update the register() function to enable the discounts:

PHP
/**
 * Registers functionality through WordPress hooks.
 */
public function register() {
	$this->enable_discount();
}

Unfortunately, it has already broken! The discount_applies() function no longer works because when we are checking for $product->is_on_sale() the WooCommerce logic in the function compares the product’s sale price with the product’s regular price, but we are filtering the regular price and are trying to determine what the regular price should be based on what discount_applies() returns – that’s an infinite loop.

This is where the enable_discount() and disable_discount() functions come in – they provide the means to prevent such a loop:

PHP
/**
 * Should given product be discounted.
 *
 * @param WC_Product $product
 * @return boolean
 */
public function discount_applies(WC_Product $product) {

	if(!is_user_logged_in()) {
		return false;
	}

	$this->disable_discount();
	$is_on_sale = $product->is_on_sale();
	$this->enable_discount();

	return !$is_on_sale;
}

Should we need the original product price at any time, it is also possible:

PHP
/**
 * @param WC_Product $product
 * @return string Original price.
 */
public function get_original_price(WC_Product $product) {
	$this->disable_discount();
	$original_price = $product->get_price();
	$this->enable_discount();
	return $original_price;
}

There is one more issue to take care of – while the variable products’ variations have the discounts applied, the price that is rendered for the whole variable product does not take this into account. There isn’t even a good way to filter it, so a lot of its’ logic needs to be duplicated, how ever here is the solution by hooking into the woocommerce_variable_price_html filter:

PHP
/**
 * Registers functionality through WordPress hooks.
 */
public function register() {
	$this->enable_discount();
	add_filter('woocommerce_variable_price_html', [$this, 'product_variation_get_price'], 10, 2);
}

/**
 * Use `woocommerce_product_variation_get_price` filter when displaying variable product price.
 *
 * @param string $price
 * @param WC_Product $product
 * @return string
 */
public function product_variation_get_price($price, $product) {

	if($this->discount_applies($product)) {

		$prices = $product->get_variation_prices(true);

		if(empty($prices['price'])) {
			$price = apply_filters('woocommerce_variable_empty_price_html', '', $product);
		} else {
			$min_price = apply_filters('woocommerce_product_variation_get_price', current($prices['price']), $product);
			$max_price = apply_filters('woocommerce_product_variation_get_price', end($prices['price']), $product);
			$min_reg_price = apply_filters('woocommerce_product_variation_get_price', current($prices['regular_price']), $product);
			$max_reg_price = apply_filters('woocommerce_product_variation_get_price', end($prices['regular_price']), $product);

			if($min_price !== $max_price) {
				$price = wc_format_price_range($min_price, $max_price);
			} elseif($product->is_on_sale() && $min_reg_price === $max_reg_price) {
				$price = wc_format_sale_price(wc_price($max_reg_price), wc_price($min_price));
			} else {
				$price = wc_price($min_price);
			}

			$price = $price . $product->get_price_suffix();
		}

		return apply_filters('woocommerce_get_price_html', $price, $product);
	}

	return $price;
}

Here’s the full code so far:

PHP
<?php

final class SKA_Logged_In_Discount {

	/**
	 * Discount - amount to multiply original price with.
	 *
	 * @var float
	 */
	protected $discount = 0.9; // 10%

	public function __construct() {

		if(!class_exists('WooCommerce')) {
			return;
		}

		$this->register();
	}

	/**
	 * Registers functionality through WordPress hooks.
	 */
	public function register() {
		$this->enable_discount();
		add_filter('woocommerce_variable_price_html', [$this, 'product_variation_get_price'], 10, 2);
	}

	/**
	 * Should given product be discounted.
	 *
	 * @param WC_Product $product
	 * @return boolean
	 */
	public function discount_applies(WC_Product $product) {

		if(!is_user_logged_in()) {
			return false;
		}

		$this->disable_discount();
		$is_on_sale = $product->is_on_sale();
		$this->enable_discount();

		return !$is_on_sale;
	}

	/**
	 * Get product price with discount.
	 *
	 * @param string $price
	 * @return string
	 */
	public function get_discounted_price($price) {

		if(!is_numeric($price)) {
			return $price;
		}

		return $price * $this->discount;
	}

	/**
	 * Apply discount to product price.
	 *
	 * @param string $price Current price.
	 * @param WC_Product $product
	 * @return string Maybe discounted price.
	 */
	public function apply_discount($price, WC_Product $product) {

		if($this->discount_applies($product)) {
			return $this->get_discounted_price($price);
		}

		return $price;
	}

	public function enable_discount() {
		add_filter('woocommerce_product_get_price', [$this, 'apply_discount'], 10, 2);
		add_filter('woocommerce_product_get_regular_price', [$this, 'apply_discount'], 10, 2);
		add_filter('woocommerce_product_variation_get_price', [$this, 'apply_discount'], 10, 2);
		add_filter('woocommerce_get_variation_price', [$this, 'apply_discount'], 10, 2);
	}

	public function disable_discount() {
		remove_filter('woocommerce_product_get_price', [$this, 'apply_discount']);
		remove_filter('woocommerce_product_get_regular_price', [$this, 'apply_discount']);
		remove_filter('woocommerce_product_variation_get_price', [$this, 'apply_discount']);
		remove_filter('woocommerce_get_variation_price', [$this, 'apply_discount']);
	}

	/**
	 * @param WC_Product $product
	 * @return string Original price.
	 */
	public function get_original_price(WC_Product $product) {
		$this->disable_discount();
		$original_price = $product->get_price();
		$this->enable_discount();
		return $original_price;
	}

	/**
	 * Use `woocommerce_product_variation_get_price` filter when displaying variable product price.
	 *
	 * @param string $price
	 * @param WC_Product $product
	 * @return string
	 */
	public function product_variation_get_price($price, $product) {

		if($this->discount_applies($product)) {

			$prices = $product->get_variation_prices(true);

			if(empty($prices['price'])) {
				$price = apply_filters('woocommerce_variable_empty_price_html', '', $product);
			} else {
				$min_price = apply_filters('woocommerce_product_variation_get_price', current($prices['price']), $product);
				$max_price = apply_filters('woocommerce_product_variation_get_price', end($prices['price']), $product);
				$min_reg_price = apply_filters('woocommerce_product_variation_get_price', current($prices['regular_price']), $product);
				$max_reg_price = apply_filters('woocommerce_product_variation_get_price', end($prices['regular_price']), $product);

				if($min_price !== $max_price) {
					$price = wc_format_price_range($min_price, $max_price);
				} elseif($product->is_on_sale() && $min_reg_price === $max_reg_price) {
					$price = wc_format_sale_price(wc_price($max_reg_price), wc_price($min_price));
				} else {
					$price = wc_price($min_price);
				}

				$price = $price . $product->get_price_suffix();
			}

			return apply_filters('woocommerce_get_price_html', $price, $product);
		}

		return $price;
	}
}

add_action('init', function() {
	new SKA_Logged_In_Discount();
});

Maybe it doesn’t suit your needs perfectly but since it’s all in simple code you can easily modify or extend it.

One response to “WooCommerce discount for logged-in users”

  1. […] 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 […]

Leave a Reply

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