This is the documentation for LemonStand V1, which has been discontinued. You can learn more and upgrade your store here.

LemonStand Version 1 Has Been Discontinued

This documentation is for LemonStand Version 1. LemonStand is now offered as a cloud-based eCommerce platform.
You can try the new LemonStand and learn about upgrading here.

Extending the Discount Engine

You can extend the discount engine with new actions and conditions.

Developing discount engine actions

Discount engine actions are classes which apply specific discounts to cart items (for cart-level discounts) or products (for catalog-level price rules). Action class names should follow the naming convention described in the developing_lemonstand_modules section. You can define new actions by adding them to the Shop module, or to custom modules.

If you add an action to the Shop module, the action PHP script should be placed to the /modules/shop/price_rule_actions directory. The action class name should have the _Action suffix. For example, class name - Shop_MyDiscount_Action, file name - /modules/shop/price_rule_actions/shop_mydiscount_action.php.

If you add an action to a custom module, the action PHP script should be placed to the module classes directory - /modules/mymodule/classes. The action class name should have a prefix in the following format: ModuleName_De_, and the _Action suffix. Example: class name - MyModule_De_SomeDiscount_Action, file name - /modules/mymodule/classes/mymodule_de_somediscount_action.php.

Discount engine action classes should extend the Shop_CartRuleActionBase class. The following methods could be defined in any discount engine action class:

  • get_name() - returns the action name. This method is required.
  • get_action_type() - determines whether the action is applicable for Discounts or Catalog Price Rules. This method is required.
  • eval_discount() - applies discounts to cart items. This method should be overridden only for Discount actions (which apply discount to cart items).
  • eval_amount() - applies discount to a product. This method should be overridden only for Catalog Price Rules actions.
  • is_per_product_action() - determines whether the action can apply discounts only to specific items in the cart. If this method returns TRUE, the Product Filter section is displayed on the action configuration form. 
     
  • build_config_form() - adds action-specific controls to the action configuration form. If your action does not have any controls, you can omit this method.

Below is a typical template of a discount action which applies discounts to cart items.

<?
  class Shop_MyCartDiscount_Action extends Shop_CartRuleActionBase
  {
    public function get_action_type()
    {
      return self::type_cart;
    }
    
    public function get_name()
    {
      return "Discount the shopping cart subtotal by fixed amount";
    }
    
    public function build_config_form($host_obj)
    {
      ...
    }
    
    public function is_per_product_action()
    {
      return false;
    }
    
    public function eval_discount(&$params, $host_obj, &$item_discount_map,
                                  &$item_discount_tax_incl_map, $product_conditions)
    {
      ...
    }
  }
?>

Below is a typical template of a discount action which applies discounts to products (applicable for the Catalog Price Rules section). 

<?

  class Shop_MyProductDiscount_Action extends Shop_RuleActionBase
  {
    public function get_action_type()
    {
      return self::type_product;
    }
    
    public function get_name()
    {
      return "By fixed amount";
    }
    
    public function build_config_form($host_obj)
    {
      ...
    }
    
    public function eval_amount(&$params, $host_obj)
    {
      ...
    }
  }

?>

Building the discount action configuration form

You can add controls to the discount action configuration form in the build_config_form(). This method builds the discount configuration form and it is similar to the define_form_fields() method of the model class, which we described in the Forms article. In this method you can add fields to the action configuration form. All field values are saved automatically and you can access them in the code which calculates discounts. The set of fields you define in the build_config_form() method depends on the action you are developing.

Below is a definition of the build_config_form() method of the Shop_ProductToPercentage_Action discount action class:

public function build_config_form($host_obj)
{
  $host_obj->add_field('discount_amount', 'Discount amount', 'full', db_float)->comment('Please 
    specify a percentage of the original product price to use as a final product price.', 'above')
    ->validation()->required('Please specify discount amount');
}

The method has a single - the Active Record object, which should be used for adding form fields. The example code creates 1 field on the configuration form - Discount amount. Usually all discount action have this field. Below is a screenshot of the Action tab of the discount rule configuration form:

To add fields to the configuration form, the code inside the build_config_form() method should call the add_field() method of the object, passed in the first method parameter. This method has 4 parameters - the field identifier, the field title, field alignment and field type. Field identifiers should be compatible with PHP variable names - they should not contain spaces, and they can contain only Latin characters and numbers. Using the third parameter you can specify a field placement on the form. Allowed parameter values are leftright and full (the default value). The fourth parameter allows to specify the field type. For currency values use the db_float type.

Applying discounts to cart items

If you are developing a discount action which should apply discounts to the cart items, you should declare the eval_discount() method in your discount action class. The method should return the total discount value applied to cart items. The method has the following parameters:

  • $params - contains a list of discount engine parameters which could be needed in the discount calculation. The array contains the following elements:
    cart_items - an array of Shop_CartItem objects, representing the cart content.
    - current_subtotal - current cart subtotal. This value takes into account discounts applied by other discount actions (if any).
    - customer - an object of class Shop_Customer, or null. Specifies a customer the order belongs to.
  • $host_obj - an object containing the action configuration values specified in the configuration form.
  • &$item_discount_map - a list of cart item identifiers and corresponding discounts. This array can be used for obtaining a value of discount applied to a cart item and should be updated by the action class. The discount action should update values for items the action applies discount to.
  • &$item_discount_tax_incl_map - the same as the $item_discount_map parameter, but with taxes included to the discount values. You do not need to maintain this parameter if you are not going to use the action in tax inclusive jurisdictions (Australia, Great Britain, and others).
  • $product_conditions - the product filter object. This object is needed to detect whether the product filter configuration allows to apply discount to a specific cart item.

The content of the eval_discount() method depends on the business logic you want to implement inside the action, but the task of any discount action is calculating the total discount amount applied to all items (taking into account item quantities) and maintaining the content of the $item_discount_map and $item_discount_tax_incl_map variables.

Let's review the typical example of the discount action, which applies fixed discount to specific items in the cart. This action is complicated, but illustrative. Below is a commented code of the action.

public function get_action_type()
{
  /*
   * This action applies discounts to the cart items
   */
  return self::type_cart;
}

public function get_name()
{
  return "Discount each cart item unit price by fixed amount";
}

public function is_per_product_action()
{
  /*
   * This action allows the product filter
   */
  return true;
}

public function build_config_form($host_obj)
{
  /*
   * The action has a single required field - the discount amount. The code
   * below adds this field to the action configuration form.
   */
  $host_obj->add_field('discount_amount', 'Discount amount', 'full', db_float)
  ->comment('Please specify an amount to subtract from cart item unit price. ', 'above')
  ->validation()->required('Please specify discount amount');
}

public function eval_discount(&$params, $host_obj, &$item_discount_map,
                              &$item_discount_tax_incl_map, $product_conditions)
{
  /*
   * Extract the cart items from the $params array and initialize discount variables
   */

  $cart_items = $params['cart_items'];
  $total_discount = 0;
  $total_discount_incl_tax = 0;

  /*
   * Determine whether the tax should be included to discounts
   */
  $include_tax = Shop_CheckoutData::display_prices_incl_tax();

  /*
   * Loop through the cart items
   */
  foreach ($cart_items as $item)
  {
    /*
     * Calculate the current product price as a difference between the original product price and discount
     * applied to it by other discount actions (if any)
     */
    $original_product_price = $item->total_single_price();
    $current_product_price = max($original_product_price - $item_discount_map[$item->key], 0);

    /*
     * The following array ($rule_params) is needed for the product filter, to determine whether
     * the discount should be applied to the current item. Usually the parameters initialization
     * is similar for all discount actions.
     */
    $rule_params = array();
    $rule_params['product'] = $item->product;
    $rule_params['item'] = $item;
    $rule_params['current_price'] = $item->single_price_no_tax(false) - $item->discount(false);
    $rule_params['quantity_in_cart'] = $item->quantity;
    $rule_params['row_total'] = $item->total_price_no_tax();
    $rule_params['item_discount'] = isset($item_discount_map[$item->key]) ? $item_discount_map[$item->key] : 0;

    /*
     * Apply the product filter. The $this->is_active_for_product() method checks whether the product is allowed 
     * by the filter configuration.
     */
    if ($this->is_active_for_product($item->product, $product_conditions, $current_product_price, $rule_params))
    {
      /*
       * Initialize the discount amount variables - just load the value from the configuration form.
       */
      $total_discount_incl_tax = $discount_value = $host_obj->discount_amount;
      
      if ($include_tax)
      {
        /*
         * If the tax inclusive mode is enabled, we need to extract the real discount value from the discount total,
         * because the discount engine works in the tax exclusive mode, but discounts are specified in tax inclusive mode.
         */
        $discount_value = Shop_TaxClass::get_subtotal($item->product->tax_class_id, $discount_value);
      }
      
      /*
       * Do not allow the discount to exceed the current product price
       */
      if ($discount_value > $current_product_price)
        $total_discount_incl_tax = $discount_value = $current_product_price;
      
      /*
       * Calculate the total discount with tax included value - apply the tax to the tax value and add it to the tax value
       */
      if ($include_tax)
        $total_discount_incl_tax = Shop_TaxClass::get_total_tax($item->product->tax_class_id, $discount_value) + $discount_value;
      
      /*
       * Increase the total discount (the method result)
       * and update the item discount maps for the tax exclusive and tax inclusive discounts.
       */
      $total_discount += $discount_value*$item->quantity;
      $item_discount_map[$item->key] += $discount_value;
      $item_discount_tax_incl_map[$item->key] += $total_discount_incl_tax;
    }
  }
  
  /*
   * Return the discount amount
   */
  return $total_discount;
}

Applying discounts to products

Catalog-level discount actions are usually simpler than cart-level actions, because they have less configuration parameters and context variables. The action class for catalog-level actions should contain the eval_amount() method. This method should return the amount of the discount applied by the rule. The method has the following parameters:

  • $params - contains a list of discount engine parameters which could be needed in the discount calculation. The array contains the following elements:
    product - an object of the Shop_Product class, representing a product the discount should be calculated for.
    - current_price - determines the current product price
  • $host_obj - an object containing the action configuration values specified in the configuration form.

Below is a code of an action which applies a percentage discount to products.

public function get_action_type()
{
  return self::type_product;
}

public function build_config_form($host_obj)
{
  /*
   * The action has a single required field - the discount amount. The code
   * below adds this field to the action configuration form.
   */
  $host_obj->add_field('discount_amount', 'Discount amount', 'full', db_float)
  ->comment('Please specify a percentage of the original product price to use as a final product price.', 'above')
  ->validation()->required('Please specify discount amount');
}

public function eval_amount(&$params, $host_obj)
{
  /*
   * Extract the product price from the parameter array
   */
  $current_price = $params['current_price'];
  
  /*
   * Calculate and return the discount value
   */
  return $current_price*$host_obj->discount_amount/100;
}

Other examples

We suggest you to check the /modules/shop/price_rule_actions/ directory for other examples of Discount Engine actions.

Next: Authoring for LemonStand Marketplace
Previous: Accessing LemonStand API from third-party scripts
Return to Extending LemonStand