CartThrob 9 documentation for ExpressionEngine commerce teams Visit CartThrob.com
CartThrob.com

Discount Plugins

This guide explains how to build a custom CartThrob discount plugin using the CartThrob 9.x plugin API. It assumes basic PHP knowledge: classes, methods, arrays, and returning values.

CartThrob ships with several working discount plugins. Start by copying one of those and changing it, rather than writing everything from scratch.

Good starter examples:

  • Cartthrob_discount_percentage_off.php for a simple percent off the cart subtotal
  • Cartthrob_discount_amount_off.php for a flat amount off
  • Cartthrob_discount_percentage_off_product.php when the discount applies only to specific products
  • Cartthrob_discount_free_shipping.php when the discount sets shipping to zero

Discount plugins return numeric amounts (float / int), not Money objects.

Discount plugins power both:

  • Automatic discounts
  • Coupon / voucher codes

Both are managed through ExpressionEngine channels and CartThrob discount settings. See the ExpressionEngine Docs if you need a refresher on channels, fields, and entries.

Start Here

A discount plugin is a PHP class that:

  1. Lives in CartThrob’s discount plugins folder
  2. Extends CartThrob\Plugins\Discount\DiscountPlugin
  3. Declares a $title
  4. Implements get_discount() and returns the discount amount

At a minimum, CartThrob needs that get_discount() method so it can calculate the cart discount.

If the discount should only apply when the cart meets certain conditions, also:

  1. implements CartThrob\Plugins\Discount\ValidateCartInterface
  2. Add validateCart(): bool
  3. Call set_error(...) when validation fails

File Location and Naming

Store discount plugins here:

system/user/addons/cartthrob/cartthrob/plugins/discount/

Rules:

  1. The filename and classname must match.
  2. The classname must begin with Cartthrob_discount.
  3. The file must begin with Cartthrob_discount.
  4. Do not start your own PHP session. CartThrob already manages session and cart state.

Example:

Filename: Cartthrob_discount_my_percent.php
Class:    Cartthrob_discount_my_percent

Minimum Working Plugin

This is the smallest useful discount plugin: a percent off the cart subtotal.

<?php

use CartThrob\Math\Number;
use CartThrob\Plugins\Discount\DiscountPlugin;

if (!defined('CARTTHROB_PATH')) {
    Cartthrob_core::core_error('No direct script access allowed');
}

class Cartthrob_discount_my_percent extends DiscountPlugin
{
    public $title = 'My Percentage Off';

    public $settings = [
        [
            'name' => 'Percentage off',
            'short_name' => 'percentage_off',
            'type' => 'text',
            'default' => '10',
            'note' => 'Enter 10 for 10%, not 0.10.',
        ],
    ];

    public function get_discount()
    {
        return $this->core->cart->subtotal()
            * abs(Number::sanitize($this->plugin_settings('percentage_off')) / 100);
    }
}

What this does:

  • Shows a Percentage off field in the discount/coupon field settings
  • Reads that setting with plugin_settings('percentage_off')
  • Converts 10 into 0.10
  • Returns subtotal * 0.10 as the discount amount

After adding the file, create or edit a discount/coupon entry, choose this plugin type, save the settings, and test in the cart.

Plugin Information

These public properties describe the plugin in the control panel.

public $title = 'My Percentage Off';
public $note = 'Optional note shown with the plugin settings.';
  • $title is required.
  • $note is optional.
  • You can use plain text, or a language key that CartThrob will look up.

Language Files

Language keys keep settings labels cleaner and make text easier to reuse.

Built-in CartThrob discount plugins store many labels in:

system/user/addons/cartthrob/language/english/cartthrob_lang.php

You can also place a plugin-specific language file next to the plugin, named like:

cartthrob_discount_my_percent_lang.php

Example language entries:

<?php

$lang = [
    'my_percentage_off_title' => 'My Percentage Off',
    'percentage_off' => 'Percentage Off',
    'percentage_off_note' => 'Enter 10 for 10%, not 0.10.',
];

Then reference those keys in the plugin:

public $title = 'my_percentage_off_title';

public $settings = [
    [
        'name' => 'percentage_off',
        'note' => 'percentage_off_note',
        'short_name' => 'percentage_off',
        'type' => 'text',
    ],
];

If CartThrob finds the language key, it shows the translated text. If not, it shows the key itself.

A separate language file is optional for a basic plugin. Plain-text $title and setting labels work fine while you are learning.

Inside the plugin, common CartThrob language lines are available through:

$this->core->lang('coupon_not_valid_for_items');

For ExpressionEngine language file conventions, see the EE language files docs.

Plugin Settings

Each plugin can define its own settings array. Every setting needs at least:

  • name
  • type
  • short_name

Optional keys:

  • default
  • options
  • note
  • size

Setting Keys

name

Required. Label shown in the control panel. Use plain text or a language key.

type

Required. Supported types commonly used by discount plugins:

  1. text
  2. textarea
  3. radio
  4. select
  5. checkbox
  6. header

text, textarea, and header store string-like values. The others usually store arrays or selected option values.

Unchecked checkboxes typically return no value.

short_name

Required. The key you use in PHP with plugin_settings('short_name').

Use letters, numbers, underscores, and hyphens only.

default

Optional. Default value for the setting.

options

Usually required for radio, select, and checkbox.

'options' => [
    'yes' => 'Yes',
    'no' => 'No',
],

The array key is the stored value. The array value is the label shown to the admin.

note

Optional help text shown with the setting. HTML is allowed.

Settings Example

public $settings = [
    [
        'name' => 'Percentage off',
        'short_name' => 'percentage_off',
        'type' => 'text',
        'default' => '10',
        'note' => 'Enter 10 for 10%.',
    ],
    [
        'name' => 'Product entry IDs',
        'short_name' => 'entry_ids',
        'type' => 'text',
        'note' => 'Separate multiple entry IDs with commas or pipes.',
    ],
];

Read settings in your plugin like this:

$percent = $this->plugin_settings('percentage_off');
$entry_ids = $this->plugin_settings('entry_ids', '');

The second argument is the fallback if the setting is missing.

Global Discount Settings

DiscountPlugin already defines shared global settings used across discount/coupon entries:

  • used_by
  • per_user_limit
  • discount_limit
  • member_groups
  • member_ids

You usually do not need to redefine those in your plugin. CartThrob applies them when validating coupons and discounts.

Your $settings array should focus on the calculation-specific fields for your plugin.

Required and Optional Methods

get_discount()

Required for every discount plugin. This is the only abstract method on DiscountPlugin.

Parameters: none
Returns: number (float or int) — the discount amount, not the discounted total

public function get_discount()
{
    return $this->core->cart->subtotal()
        * abs(Number::sanitize($this->plugin_settings('percentage_off')) / 100);
}

Important:

  • Return the discount portion only. If the subtotal is 100 and the discount is 10%, return 10, not 90.
  • Return a number. Do not return a formatted currency string.
  • Percent settings are usually entered like 10. Convert them with / 100 before multiplying.
  • Use CartThrob\Math\Number::sanitize(...) to clean admin input before math.

Free-shipping style plugins may return 0 and change shipping instead:

public function get_discount()
{
    $this->core->cart->set_shipping(0);

    return 0;
}

validateCart(): bool

Optional, but needed when the discount should only apply if the cart qualifies.

Implement ValidateCartInterface and return true or false.

Parameters: none
Returns: boolean

use CartThrob\Plugins\Discount\ValidateCartInterface;

class Cartthrob_discount_my_product extends DiscountPlugin implements ValidateCartInterface
{
    public function validateCart(): bool
    {
        // return true if the cart qualifies
        // return false and set_error(...) if it does not
    }
}

For coupon codes, a failed validation can show an error message.
For automatic discounts, failed validation usually means the discount is simply skipped.

initialize($plugin_settings = [], $defaults = [])

Optional.

Use this when you need to prepare plugin-wide values once, such as adjusting settings options dynamically.

Parameters: $plugin_settings = [], $defaults = []
Returns: usually $this

public function initialize($plugin_settings = [], $defaults = [])
{
    parent::initialize($plugin_settings, $defaults);

    // optional setup here

    return $this;
}

set_error($error) / error()

Use set_error() inside validateCart() when a coupon should explain why it is invalid.

$this->set_error($this->core->lang('coupon_not_valid_for_items'));

toString($data)

Optional. Many bundled plugins implement this for Order Manager / human-readable discount summaries. It is not required for cart discount calculation.

How CartThrob Uses Discount Plugins

Understanding the call sites helps you write the right methods.

Coupons

When a coupon code is in the cart and valid, CartThrob:

  1. Loads the coupon entry data
  2. Creates the selected discount plugin
  3. Calls get_discount()
  4. Adds that amount into the cart discount total

If the plugin implements ValidateCartInterface, CartThrob also calls validateCart() while validating the coupon.

Automatic discounts

CartThrob also loops configured automatic discount entries, creates each plugin, and calls get_discount().

Discount vs shipping override

Most plugins return a money amount that reduces the order.
Some plugins, like free shipping, return 0 and call:

$this->core->cart->set_shipping(0);

You can also force a cart total in advanced cases:

$this->core->cart->set_total(0); // free order

Use those carefully. They override normal cart calculations.

Useful Cart Helpers

These are the helpers you will use most often inside discount plugins.

Plugin settings

$this->plugin_settings('percentage_off');
$this->plugin_settings('entry_ids', '');

Cart totals

$this->core->cart->subtotal();
$this->core->cart->shipping();
$this->core->cart->count();

Cart items

foreach ($this->core->cart->items() as $item) {
    $item->quantity();
    $item->price();
    $item->product_id();
    $item->weight();
}

Product-specific discounts often check $item->product_id() against a list of entry IDs.

Product IDs currently in the cart

$this->core->cart->product_ids();

Useful for validateCart() checks.

Customer info

$this->core->cart->customer_info();
$this->core->cart->customer_info('email_address');

Number cleanup

use CartThrob\Math\Number;

$clean = Number::sanitize($this->plugin_settings('amount_off'));

Prefer Number::sanitize() for settings input.

ExpressionEngine access

Use ee():

ee()->session->userdata('member_id');

Do not use $this->EE =& get_instance(); in new plugins.

If you are new to EE add-on development, start with the ExpressionEngine Docs and the Add-on Development Overview.

Validating the Cart

Use ValidateCartInterface when the discount depends on cart contents.

Example pattern from Percentage Off Product:

public function validateCart(): bool
{
    $valid = false;

    if ($this->plugin_settings('entry_ids') && $entry_ids = preg_split(
        '#\s*[,|]\s*#',
        trim($this->plugin_settings('entry_ids'))
    )) {
        $valid = count(array_intersect($this->core->cart->product_ids(), $entry_ids)) > 0;
    }

    if (!$valid) {
        $this->set_error($this->core->lang('coupon_not_valid_for_items'));
    }

    return $valid;
}

Then in get_discount(), calculate only against the eligible items.

You do not need validateCart() for simple whole-cart discounts like percentage off or amount off.

Examples

Example 1: Percentage off subtotal

Same pattern as Cartthrob_discount_percentage_off.php.

<?php

use CartThrob\Math\Number;
use CartThrob\Plugins\Discount\DiscountPlugin;

if (!defined('CARTTHROB_PATH')) {
    Cartthrob_core::core_error('No direct script access allowed');
}

class Cartthrob_discount_percentage_off extends DiscountPlugin
{
    public $title = 'percentage_off';

    public $settings = [
        [
            'name' => 'percentage_off',
            'short_name' => 'percentage_off',
            'note' => 'percentage_off_note',
            'type' => 'text',
        ],
    ];

    public function get_discount()
    {
        return $this->core->cart->subtotal()
            * abs(Number::sanitize($this->plugin_settings('percentage_off')) / 100);
    }
}

Example 2: Flat amount off

Same pattern as Cartthrob_discount_amount_off.php.

public function get_discount()
{
    return abs(Number::sanitize($this->plugin_settings('amount_off')));
}

Example 3: Percentage off specific products

Use Cartthrob_discount_percentage_off_product.php as your model when you need:

  1. ValidateCartInterface
  2. Entry ID matching
  3. Per-item discount accumulation

Key pieces:

use CartThrob\Plugins\Discount\DiscountPlugin;
use CartThrob\Plugins\Discount\ValidateCartInterface;

class Cartthrob_discount_percentage_off_product extends DiscountPlugin implements ValidateCartInterface
{
    public function get_discount()
    {
        // Loop cart items, discount matching product IDs, return total discount
    }

    public function validateCart(): bool
    {
        // Return true only if at least one eligible product is in the cart
    }
}

Example 4: Free shipping

Same pattern as Cartthrob_discount_free_shipping.php.

public function get_discount()
{
    $this->core->cart->set_shipping(0);

    return 0;
}

Checklist

Before testing your plugin:

  1. File is in system/user/addons/cartthrob/cartthrob/plugins/discount/
  2. Filename and classname match
  3. Classname starts with Cartthrob_discount
  4. Class extends CartThrob\Plugins\Discount\DiscountPlugin
  5. get_discount() exists and returns a number
  6. Percent settings are converted with / 100 before multiplying
  7. Settings use valid name, type, and short_name keys
  8. If the discount depends on cart contents, the class implements ValidateCartInterface and provides validateCart()
  9. A discount or coupon entry uses your plugin type and is saved
  10. Test with a qualifying cart, a non-qualifying cart, and coupon error messaging if relevant

If the plugin does not appear as a discount type, check the classname prefix and file location first. If the discount calculates as zero or looks like a final total instead of a discount amount, check that get_discount() returns the discount portion only and that percent values are decimals like 0.10.

ExpressionEngine References