Coupon Effects ?

How to create a new type of Coupon ?

Our example will be based on a Coupon giving a particular product (a goodies) to any customer entering it and meeting the Coupon Condition. In case you have trouble following our explanation, you can find the whole code as a module on GitHub.

1) Implement : A Coupon has to implement the Thelia\Coupon\Type\CouponInterface Interface.

In order to save you some time, we advise to simply have your GiveProduct class extends our Thelia\Coupon\Type\CouponAbstract class.

Please create your new GiveProduct type class in your module. Preferably in the directory local/modules/MyModule/Coupon/Type/.

2) Inform : In order to inform Thelia2 about the existence of your new Coupon you would have to register it as a Service :

local/modules/MyModule/Config/config.xml

<services>
    <service id="thelia.coupon.type.give_product" class="CouponGiveProduct\Coupon\Type\GiveProduct">
        <argument type="service" id="thelia.facade" />
        <tag name="thelia.coupon.addCoupon"/>
    </service>
</services>

Basically you create a new service having thelia.coupon.type.give_product as id building the class CouponGiveProduct\Coupon\Type\GiveProduct with the facade id thelia.facade which is also another service. And since we tag it with thelia.coupon.addCoupon Thelia2 will automatically add it to its Coupon list.

3) I18n : You would then have to implements 3 simple i18n methods describing the Coupon :

local/modules/MyModule/Coupon/Type/GiveProduct.php

  • getName() which contains the i18n name of your coupon. Ex: “Add a free product to the customer cart”
  • getInputName() which contains the i18n name of the input effect. Ex: “Product sale element id added to the cart”
  • getToolTip() which contains the i18n description of your Coupon. Ex : “This Coupon will give the associated product to the customer cart. The Coupon will make sure one order can get only one time the selected free product at the given quantity.”

4) Behavior : Then the method responsible for the Coupon behavior. This is where all the strength of our Coupon module is :

The method exec() contains all the Coupon logic. It performs the Coupon effect and return the discount (which can be 0).

caution Beware it will be executed whenever the coupon is entered, whether the order is paid or not.

Example : local/modules/MyModule/Coupon/Type/GiveProduct.php

/**
 * Return effects generated by the coupon
 * A new product in the cart
 *
 * @return float The discount
 */
public function exec()
{
    // The CartItem price will be set to 0
    // So no need to return a discount
    $discount = 0;

    // Since the exec method will be called each time the cart checks its integrity
    //  We need to check if the free product has already been inserted in the Cart
    if (!$this->isAlreadyInCart($this->productSaleElementsId)) {

        /** @var ProductSaleElements $productToGive */
        $productToGive = $this->getFreeProduct();
        $this->addProductToCustomerCart($productToGive);
    }

    return $discount;
}

You will be able to see the Coupon logic (Product retrieval, CartItem insertion, Event and Action) on GitHub.

5) Type (service id) : You would then have to implements this attribute :

local/modules/MyModule/Coupon/Type/GiveProduct.php

/** @var string Service Id  */
protected $serviceId = 'thelia.coupon.type.give_product';

Where give_product is the unique id of your Coupon set in step 2). This is the name of the Service.

6) Custom inputs : You might need to have more parameter than the default amount and percent ?

In our example we need to add product_sale_element_id and quantity parameters. You can declare constants with the name of your inputs in order to ease the maintainability :

local/modules/MyModule/Coupon/Type/GiveProduct.php

/** ProductSaleElement id input name */
const INPUT_PRODUCT_SALE_ELEMENT_ID_NAME = 'product_sale_element_id';
/** Product quantity input name */
const INPUT_QUANTITY_NAME = 'quantity';

You can also implement these inputs as attributes and implements their getter. They might be useful for you later.

local/modules/MyModule/Coupon/Type/GiveProduct.php

/** @var int Product Sale Element id you wish to offer */
protected $productSaleElementsId = 0;

/** @var int Quantity of Free product given for a Coupon */
protected $quantity = 1;

/**
 * Get Product Sale Element id to give
 *
 * @return int
 */
public function getProductSaleElementsId()
{
    return $this->productSaleElementsId;
}

/**
 * Get quantity to give
 *
 * @return int
 */
public function getQuantity()
{
    return $this->quantity;
}

In order to feed these attributes you would need to extend the default set() method.

/**
 * Set Coupon
 *
 * @param FacadeInterface $facade                     Provides necessary value from Thelia
 * @param string          $code                       Coupon code (ex: XMAS)
 * @param string          $title                      Coupon title (ex: Coupon for XMAS)
 * @param string          $shortDescription           Coupon short description
 * @param string          $description                Coupon description
 * @param array           $effects                    Coupon effects params
 * @param bool            $isCumulative               If Coupon is cumulative
 * @param bool            $isRemovingPostage          If Coupon is removing postage
 * @param bool            $isAvailableOnSpecialOffers If available on Product already
 *                                                    on special offer price
 * @param bool            $isEnabled                  False if Coupon is disabled by admin
 * @param int             $maxUsage                   How many usage left
 * @param \Datetime       $expirationDate             When the Code is expiring
 *
 * @return $this
 */
public function set(
    FacadeInterface $facade,
    $code,
    $title,
    $shortDescription,
    $description,
    array $effects,
    $isCumulative,
    $isRemovingPostage,
    $isAvailableOnSpecialOffers,
    $isEnabled,
    $maxUsage,
    \DateTime $expirationDate
)
{
    // We use the default behavior we will extend
    parent::set(
        $facade, $code, $title, $shortDescription, $description, $effects, $isCumulative, $isRemovingPostage, $isAvailableOnSpecialOffers, $isEnabled, $maxUsage, $expirationDate
    );

    if (isset($effects[self::INPUT_PRODUCT_SALE_ELEMENT_ID_NAME])) {
        $this->productSaleElementsId = $effects[self::INPUT_PRODUCT_SALE_ELEMENT_ID_NAME];
    }
    if (isset($effects[self::INPUT_QUANTITY_NAME])) {
        $this->quantity = $effects[self::INPUT_QUANTITY_NAME];
    }

    return $this;
}

Right now, Thelia2 won’t be able to draw these new inputs in the BackOffice. As it is a Thelia2 key feature, we chose to give you the opportunity to customize the way you want these inputs will to be displayed. In this way you will be able to focus on the ergonomic rather than be stuck on a rigid structure.

local/modules/MyModule/Coupon/Type/GiveProduct.php

/**
 * Draw the input displayed in the BackOffice
 * allowing Admin to set its Coupon effect
 *
 * @return string HTML string
 */
public function drawBackOfficeInputs()
{
    $labelProductSaleElementId = $this->getInputName();
    $labelQuantity = $this->getInputQuantityName();
    $value = $this->productSaleElementsId;

    $innerSelectHtml = $this->drawBackOfficePSESelect($value);

    $html = '
            <input type="hidden" name="thelia_coupon_creation[' . self::INPUT_AMOUNT_NAME . ']" value="0"/>
            <div class="form-group input-' . self::INPUT_PRODUCT_SALE_ELEMENT_ID_NAME . '">
                <label for="' . self::INPUT_PRODUCT_SALE_ELEMENT_ID_NAME . '" class="control-label">' . $labelProductSaleElementId . '</label>
                <select id="' . self::INPUT_PRODUCT_SALE_ELEMENT_ID_NAME . '" class="form-control" name="' . self::INPUT_EXTENDED__NAME . '[' . self::INPUT_PRODUCT_SALE_ELEMENT_ID_NAME . ']' . '" >' . $innerSelectHtml . '</select>
            </div>
            <div class="form-group input-' . self::INPUT_QUANTITY_NAME . '">
                <label for="' . self::INPUT_QUANTITY_NAME . '" class="control-label">' . $labelQuantity . '</label>
                <input id="' . self::INPUT_QUANTITY_NAME . '" class="form-control" name="' . self::INPUT_EXTENDED__NAME . '[' . self::INPUT_QUANTITY_NAME . ']' . '" type="text" value="' . $this->quantity . '"/>
            </div>
        ';

    return $html;
}

Putting HTML into PHP class file is not really a good practice. If needed you could draw your inputs in a Smarty template and render it via the SmartyParser.

7) Inputs saving : At this moment Thelia2 is not able to guess what it will have to serialize as JSON in the table Coupon column serialized_effects. In order to teach it how to save your new parameters simply extends this attribute in your GiveProduct type class in your module.

local/modules/MyModule/Coupon/Type/GiveProduct.php

/** @var array Extended Inputs to manage */
protected $extendedInputs = array(
    self::INPUT_PRODUCT_SALE_ELEMENT_ID_NAME,
    self::INPUT_QUANTITY_NAME
);

Your brand new custom Coupon is ready. You just have to enable your module.