Extending Discounts

By Andrew Embler, concrete5 CTO.

Before you Begin

Concepts

Discount types differ slightly from payment gateways and shipping types in that there is both a type of a discount (which you'll learn how to create in this tutorial) and an instance of a discount. For example, a type of discount might be a "Percentage Off Coupon" while an instance of the discount might be "Christmas Deals - 25% Off Everything."

You create the type first, then you add instances of it in the discounts section of your eCommerce dashboard. That's because each instance of a discount type can have its own start and end dates, its own name, and coupon code(s).

Setup your Environment

You'll need to create a directory named "core_commerce" in the models/ directory, at the root of your concrete5 install. Within this directory, create a "discount" directory, and within this, create a "types" directory. From the root of your concrete5 install, the path to this directory should be

models/core_commerce/discount/types/

This is your website's local discount types directory, and will be referred to as such from within this tutorial.

Creating the Discount Types Directory

Before you can write code for your discount type, you will need to:

  1. Choose a handle for your discount type. This is a short name that will match the directory that your discount type resides in. This should be descriptive, but be completely lowercased and contain no spaces. For example, the handle of our Free Shipping discount type is "free_shipping" .

  2. Create a directory with the same name as your discount type's handle in the local discount types directory (referenced above.) e.g. models/core_commerce/discount/types/your_discount_type

The Components of a Discount Type

There are several items that comprise a discount type. These are all files that exist within the directory you created for your specific discount type.

  1. The controller file, named "controller.php" This file is required. It should contain one PHP class. Like blocks or other items in concrete5, the controller class must be named appropriately. It must begin with "CoreCommerce", then contain the camel-cased name of the discount type, and finish with "DiscountTypeController." If your discount type is named "free_shipping_plus", your class would be CoreCommerceFreeShippingPlusDiscountTypeController. The class must extend the CoreCommerceDiscountController class.

  2. An optional db.xml file. This is an XML representation of any database tables required by the discount type. This file will automatically be parsed and executed when the discount type is installed. This file is in the AXMLS format. It is widely used throughout concrete5.

  3. The additional details form for this discount type. If it exists, it is named "type_form.php" This is displayed to site administrators when they add a new instance of the discount type, or edit an existing instance of the discount type. This form is optional.

Example 1: The Free Shipping Discount Type

The free shipping discount type is simply a way for store administrators to offer free shipping to their customers via discount.

type_form.php

When configuring the free shipping discount type through the dashboard, you'll see the contents of the additional details form (type_form.php), which are simply some concrete5 configuration form elements with names for this type. For example, for the free shipping discount we ask for a minimum purchase amount, and allow you to choose a shipping method that this applies to (this hooks into all available shipping methods to grab their list of methods and display it here.)

You won't see any form tags or submit buttons in the additional details form, however, because this is handled autmomatically by concrete5, by a method in the controller file.

controller.php

The controller file for the free shipping type contains the following components. (Note: additional methods other than these are present in the free discount type controller, but these are the ones that are required and used by all discount types. Other methods are used specifically by the free discount type controller.)

type_form()

This method is automatically run when additional details form is displayed in the dashboard. These lines of code simply retrieve the bits of data we're saving with this method form. There is some additional code here which is responsible for grabbing all currently installed shipping methods, in order to populate the select menu in the type form.

This ensures that previously saved data accurately populates the additional details form.

validateDiscount()

This is automatically run when the additional details form is submitted. If you need any specific errors to return when users are configuring your shipping type through the dashboard, use the concrete5 validation error handler here. If you return an error object that has errors (e.g. you have added at least one error using ValidationErrorHelper::add()) the save will fail.

As you can see here, we're using this method to enforce a minimum amount for the free shipping discount (although that amount CAN be zero, in which case the discount is automatically applied.)

save()

This method is run when an instance of the discount type is created or updated.

delete()

This method is run when an instance of the discount type is deleted. It is responsible for cleaning up any old data.

validate()

This method is run when the discount is interacted with on the front-end of the site, during the checkout process. For example, our free shipping discount front-end validation routine is responsible for checking whether the minimum threshold for the particular discount has been met.

$db = Loader::db();
$r = $db->GetOne('select minimumPurchase from CoreCommerceDiscountTypeFreeShipping where discountID = ?', array($this->discount->getDiscountID()));
$ec = CoreCommerceCart::get();
$total = $ec->getBaseOrderTotal();
if ($total < $r) {
    $ve = Loader::helper('validation/error');
    Loader::library('price','core_commerce');
    $ve->add(t('In order to use this code, your cart must contain items totalling %s or more.', CoreCommercePrice::format($r)));
    return $ve;
}

deleteDiscount()

This method is run when the discount is removed from an order.

applyDiscount()

This method is run when a discount is applied to an order. As you can see, in the free shipping discount, we pass most of the validation testing to a protected method named validateDiscountToOrder, which is a custom method just for the free shipping discount type. It is responsible for testing whether the discount conditions for this order are met.

Further Examples

Once you understand how the free shipping discount type works, it will be useful to check out the "basic discount" type.

Install the Discount Type on your Site

Finally, when your discount type is written and the various files present within the directory are completed (or at the very least ready to be tested) you'll need to install your discount type to make the eCommerce addon aware of it. Head to Dashboard > eCommerce > Discounts. Then, click the "Manage Discount Types" link. Within the "Custom Discount Types" section of the page, you should see your new type listed. Click install. Then return to the Discounts page, and you should be able to add a new instance of your discount type.

Converting the discount type into a package (Requires eCommerce 1.6 or greater)

If you'd like to offer the discount type to someone else, or on the concrete5 marketplace, you'll need to package it up. You can read more about the package format in the concrete5 developer documentation. To package up your discount type, simply create a package (complete with a controller script and an icon.png graphic), then create a models/ directory within the package. Within this directory create a "core_commerce" directory. Then create a "discount" and then a "types" directory beneath this directory. Finally, move your discount type directory (which contains the PHP files) into this directory.

Your shipping type package will then be structured as follows

core_commerce_your_discount_type_handle/models/core_commerce/discount/types/your_discount_type

Finally, edit your package's controller.php file, and ensure that these lines are in the install() method:

$pkg = parent::install();
Loader::model('discount/type', 'core_commerce');
CoreCommerceDiscountType::add('your_discount_type', 'Discount Type Name', $pkg);

This will install the discount type when the package is installed.