That Software Guy! eCommerce Starts Here!

Advanced Configuration for Zen Cart Discounting Modules

THESE ARE ADVANCED INSTRUCTIONS. Do not attempt this sort of change unless you are a skilled programmer. I am available for hire to do customizations like this if you require help.

This page describes approaches to complex configurations for the following Zen Cart mods: The fact that many of my modules can be configured in code in their setup functions() means that you can write logic using built in Zen Cart functions to do specific things. I will provide a few examples:

Example 1: Using Better Together, with exceptions to a category wide discount

Suppose you have (master) category 10, and you want to give a 2-for-1 discount on every product in that category. The Better Together configuration is:

    function setup() { 
       $this->add_twoforone_cat(10);
    }


Now suppose you want to do the same thing, except for products 4 and 5.

    function setup() { 
         global $db; 
         $sql = "SELECT products_id FROM " . TABLE_PRODUCTS . " WHERE master_categories_id='10'";
         $prodlist = $db->Execute($sql);
         while (!$prodlist->EOF) { 
            $prod = (int)$prodlist->fields['products_id']; 
            if ($prod != 4 && $prod != 5) {
               $this->add_twoforone_prod($prod);
            }
            $prodlist->MoveNext();
         }
    }


Note the change from add_twoforone_cat with a category, to add_twoforone_prod with a specific product.

Example 2: Using Combination Discounts to create a category-wide 3-for-2

Suppose you have (master) category 10, and you want to give a 3-for-2 discount on every product in that category. Better Together has add_twoforone_prod, but there is no such configuration for Combination Discounts, it must be hand coded. configuration is:

    function setup() { 
         global $db; 
         $sql = "SELECT products_id FROM " . TABLE_PRODUCTS . " WHERE master_categories_id='10'";
         $prodlist = $db->Execute($sql);
         while (!$prodlist->EOF) { 
            $prod = (int)$prodlist->fields['products_id']; 
            $this->add_linkage(PROD,$prod, 2, PROD, $prod, 1, "%", 100); 
            $prodlist->MoveNext();
         }
    }


Example 3: Using Table Discounts to do per item discounting across a category

Suppose you have a master category 8, and you want to offer quantity discounting of this category, with 10% off on more than 10 items or 20% off on more than 20. The Table Discounts syntax is

    function setup() { 
       $this->add_table("Buy 10 items in category 8, get 10% off; buy 20, and get 20% off"); 
          $this->set_constraint(CAT, 8);
          $this->set_discount("%", 10, 10, 20, 20);
    }


Now suppose you want to do the same thing, but on a per item basis: buy 10 of any single item in category 8, get 10% off; buy 20, get 20% off.

    function setup() { 
         global $db; 
         $sql = "SELECT products_id FROM " . TABLE_PRODUCTS . " WHERE master_categories_id='8'";
         $prodlist = $db->Execute($sql);
         while (!$prodlist->EOF) { 
            $prod = (int)$prodlist->fields['products_id']; 
            $this->add_table("Buy 10, get 10% off, buy 20, get 20% off on " . zen_get_products_name($prod));
                  $this->set_constraint(PROD, $prod);
                  $this->set_discount("%", 10, 10, 20, 20);
            $prodlist->MoveNext();
         }
   }


Again, note the change from setting the constraint on a CAT basis to a PROD basis.

Example 4: Using Big Chooser to give buy-one-get-one for a group of products across multiple categories

Suppose products 2, 3, 5, 7, 11, 13, 17, 19 exist, are all Green Teas, but are not in a single category. If you want to offer a buy one, get one half off sale, then rather than coding 8 different discounts, you can use a foreach loop and do it in code:
    function setup() { 
        $list = array(22, 3, 5, 7, 11, 13, 17, 19);
        foreach ($list as $i) {
           $this->add_condition("Buy any Green Tea and get a second of same Tea for Half off", true);
           $this->set_constraint(PROD, $i, 1);
           $this->set_discount(PROD, $i, 1, "%", 50);
        }
    }


Example 5: Dealing with fractional quantities in Better Together, Combination Discounts and Big Chooser

Some shops, such as those selling cloth or weighed goods, find it advantageous to set Admin->Configuration->Stock->Product Quantity Decimals to 2 instead of 0, and set the per product minimums to a fractional value. But my discount modules assume any quantity in the cart is at least 1 (i.e. it suffices to meet the condition in a linkage). To overcome this, simply round the quantity of each item in the cart to an integer value, and skip over the items which round to 0.

Here's the change for Better Together. In the function calculate_deductions(), change

       // Build discount list 
       for ($i=0, $n=sizeof($products); $i<$n; $i++) {
            $discountable_products[$i] = $products[$i]; 
       }

to
       // Build discount list 
       for ($i=0, $n=sizeof($products); $i<$n; $i++) {
            $products[$i]['quantity'] = (int)$products[$i]['quantity'];
            if ($products[$i]['quantity'] == 0) continue; 
            $discountable_products[] = $products[$i]; 
       }


Here's the change for Combination Discounts. In the function calculate_deductions(), change

       $products = array();
       for ($i = 0; $i < sizeof($all_products); $i++) { 
           for ($j = 0; $j < $all_products[$i]['quantity']; $j++) {
               $newprod = array(); 
               $newprod['price'] = $all_products[$i]['price']; 
               $newprod['final_price'] = $all_products[$i]['final_price']; 
               $newprod['id'] = $all_products[$i]['id']; 
               $newprod['category'] = $all_products[$i]['category']; 
               $newprod['quantity'] = 1; 
               $products[] = $newprod; 
           }
       }

to
       $products = array();
       for ($i = 0; $i < sizeof($all_products); $i++) { 
           $all_products[$i]['quantity'] = (int)$all_products[$i]['quantity'];

           if ($all_products[$i]['quantity'] == 0) continue; 
           for ($j = 0; $j < $all_products[$i]['quantity']; $j++) {
               $newprod = array(); 
               $newprod['price'] = $all_products[$i]['price']; 
               $newprod['final_price'] = $all_products[$i]['final_price']; 
               $newprod['id'] = $all_products[$i]['id']; 
               $newprod['category'] = $all_products[$i]['category']; 
               $newprod['quantity'] = 1; 
               $products[] = $newprod; 
           }
       }


Here's the change for Big Chooser. In the function calculate_deductions(), change

       for ($i = 0; $i < sizeof($all_products); $i++) { 
          $newprod = array(); 
          $newprod['price'] = $all_products[$i]['price']; 
          $newprod['final_price'] = $all_products[$i]['final_price']; 
          $newprod['id'] = $all_products[$i]['id']; 
          $newprod['category'] = $all_products[$i]['category']; 
          $newprod['quantity'] = $all_products[$i]['quantity'];
          $newprod['attributes'] = $all_products[$i]['attributes'];
          $newprod['manuf'] = zen_get_products_manufacturers_id($all_products[$i]['id']);
          $products[] = $newprod; 
       }

to
       for ($i = 0; $i < sizeof($all_products); $i++) { 
          $newprod = array(); 
          $newprod['price'] = $all_products[$i]['price']; 
          $newprod['final_price'] = $all_products[$i]['final_price']; 
          $newprod['id'] = $all_products[$i]['id']; 
          $newprod['category'] = $all_products[$i]['category']; 
          $all_products[$i]['quantity'] = (int) $all_products[$i]['quantity'];
          if ($all_products[$i]['quantity'] == 0) continue; 
          $newprod['quantity'] = $all_products[$i]['quantity'];
          $newprod['attributes'] = $all_products[$i]['attributes'];
          $newprod['manuf'] = zen_get_products_manufacturers_id($all_products[$i]['id']);
          $products[] = $newprod; 
       }


Example 6: Restricting discounts to operating within specific dates

This topic is covered in my article Timing your Discounts in Zen Cart Mods.

Example 7: Using Big Chooser to give "Buy one, get one 30% off," and Buy two, get one 50% off" across all products

For discount arrangements like this, always put the highest discount first, then work down to the lowest. Here, rather than selecting all product id's and iterating over the list, we'll just figure out the maximum value, and hit all values from one up to that value.

Note that this is for identical products - it's a BOGO. To see how to do this for any products of lesser value, see example 8.

in code:
    function setup() { 
         global $db; 
         $sql = "SELECT MAX(products_id) AS maxprid FROM " . TABLE_PRODUCTS; 
         $prodlist = $db->Execute($sql);
         $maxprid = $prodlist->fields['maxprid']; 
         for ($i = 1; $i <= $maxprid; $i++) { 
           $this->add_condition("Buy two product " . $i . " and get a third for 50% off", true);
           $this->set_constraint(PROD, $i, 2);
           $this->set_discount(PROD, $i, 1, "%", 50);

           $this->add_condition("Buy one product " . $i . " and get a second for 30% off", true);
           $this->set_constraint(PROD, $i, 1);
           $this->set_discount(PROD, $i, 1, "%", 30);

        }
    }


Note that if you are doing only one of these discounts, you should take a look at BOGO Discount.

Example 8: Using Big Chooser to give "Buy one, get any lower priced item 30% off," and "Buy two, get any lower priced item 50% off" across all products

Again, for discount arrangements like this, always put the highest discount first, then work down to the lowest.

I like discounts like this better than the one shown in example 7 because sometimes people don't *want* a second identical item. It's also more straightforward to code.

in code:
    function setup() { 
           $this->add_condition("Buy two items, get a third of lesser value for 50% off", true);
           $this->set_constraint(MINPRICE, 0.01, 2);
           $this->set_discount(MINPRICE, 0.01, 1, "%", 50);

           $this->add_condition("Buy one item, get a second of lesser value for 30% off", true);
           $this->set_constraint(MINPRICE, 0.01, 1);
           $this->set_discount(MINPRICE, 0.01, 1, "%", 30);

    }


Note that if you are doing only one of these discounts, you should take a look at BOGO Discount.

Example 9: Using Big Spender, create a "With any purchase, get a free item X" discount, where X is configured through the admin panel.

The first step is to create the config item. Don't use the prefix MODULE_ORDER_TOTAL_BIGSPENDER_DISCOUNT_ because if you do, then turning off Big Spender (using Admin->Modules->Order Total->Remove) will delete this config item. Go to Admin->Tools->Install SQL Patches, and in the query window, type
INSERT INTO configuration (configuration_title, configuration_key, configuration_value, configuration_description, 
configuration_group_id, sort_order, last_modified, date_added, use_function, set_function) 
VALUES ('Free Item', 'BIGSPENDER_FREE_ITEM', '5519', 
'This is the free item that will be given by Big Spender.', 1, 23, now(), now(), NULL, NULL);

then, configure Big Spender as follows:

   function setup() { 
         global $db; 
         $free_prid = BIGSPENDER_FREE_ITEM; 
         // Do all the checks to make sure that this item can be added
         $button_check = $db->Execute("SELECT product_is_call, products_status, products_quantity FROM " . TABLE_PRODUCTS . 
                  " WHERE products_id = '" . (int)$free_prid. "'");
         if ( ($button_check->fields['product_is_call'] != '1') && 
              ($button_check->fields['products_status'] != '0') && 
              (($button_check->fields['products_quantity'] > 0) ||
               ($button_check->fields['products_quantity'] <= 0 
                    and SHOW_PRODUCTS_SOLD_OUT_IMAGE == '1')) && 
              (zen_get_products_allow_add_to_cart($free_prid) == 'Y') ) {
            $this->add_threshold(0.01, 'With any purchase, get a free '. zen_get_products_name($free_prid) , false);
            $this->set_negative_constraint(PROD,$free_prid);
            $this->set_discount(PROD,$free_prid,1, "%",100);
         }
    }


Example 10: Using Big Chooser to create a dynamically selected kit discount

Big Chooser can be configured to create kit discounts for Zen Cart with static lists of categories and products. But what if the kit needs to dynamically search for items which could change?

We'll take this kit:
$this->add_condition("Buy a foo, a bar, a baz and get 20% off the set", false);
      $this->set_choice_constraint(1, PROD, 11, PROD, 12, PROD, 13); // foo
      $this->set_choice_constraint(1, PROD, 19, PROD, 7); // bar
      $this->set_choice_constraint(1, PROD, 20, PROD, 16, PROD, 14); // baz
      $this->set_cart_discount("%", 20, CART_DISCOUNT_CONSTRAINTS_ONLY);
and add the condition, "an item with the word 'Test' in its name."

in code:
    function setup() { 
       global $db; 
       $sql = "SELECT products_id FROM " . TABLE_PRODUCTS_DESCRIPTION . " WHERE products_name LIKE '%Test%'";
       $test_products= $db->Execute($sql);
       $prodlist = array();
       $prodlist[] = 1; // pick 1 from the list to follow
       while (!$test_products->EOF) {
          $prod = (int)$test_products->fields['products_id']; // cast as int
          $prodlist[] = PROD;
          $prodlist[] = $prod; 
          $test_products->MoveNext();
       }
    
       // Now do the discount 
       $this->add_condition("Buy a foo, a bar, a baz, and a Test, and get 20% off the set", false);
          $this->set_choice_constraint(1, PROD, 11, PROD, 12, PROD, 13); // foo
          $this->set_choice_constraint(1, PROD, 19, PROD, 7); // bar
          $this->set_choice_constraint(1, PROD, 20, PROD, 16, PROD, 14); // baz
          call_user_func_array(array($this, 'set_choice_constraint'), $prodlist);
          $this->set_cart_discount("%", 20, CART_DISCOUNT_CONSTRAINTS_ONLY);
    }
There are a few interesting things to note in this example:
  • The parameters to set_choice_constraint must be passed in as an array, and the function called with call_user_func_array. You cannot simply build a string and pass it to set_choice_constraint.
  • Because set_choice_constraint is a member function, it needs to be passed to call_user_func_array in this way.




Example 11: Setting cross sells for all products using Better Together Admin.

Most people set cross sells on a per product basis, but this customer wanted a single set of cross sells for all products.

Modify ./includes/modules/better_together_admin.php to skip over prod to prod cross sells in the while loop it uses, i.e.
     if ($units == 'X' && $lt == 'BT_PROD2PROD') {
        continue; 
     }
Then modify the Better Together Setup to add cross sells.

   function setup() {
      
      // Using Better Together Admin?  Uncomment this out
      if (!IS_ADMIN_FLAG) {
         require(DIR_WS_MODULES . 'better_together_admin.php');
      }

      // Add cross sells globally 
      global $db;   
      $discounts_query_raw = "SELECT * FROM " . TABLE_BETTER_TOGETHER_ADMIN . " WHERE ACTIVE = 'Y' AND (discount_units = 'X') AND (linkage_type = 'BT_PROD2PROD') AND (start_date <= now() OR start_date = '0001-01-01') AND (end_date >= now() OR end_date = '0001-01-01') ORDER BY id DESC "; 
      $discounts = $db->Execute($discounts_query_raw);
      $global_xsells = array(); 
      while (!$discounts->EOF) {
         $lt = $discounts->fields['linkage_type'];
         $fld1 = (int)$discounts->fields['field1'];
         $fld2 = (int)$discounts->fields['field2'];
         $units = $discounts->fields['discount_units'];
         $amount = round($discounts->fields['discount_amount'],2);
         $global_xsells[] = $fld2; 
         $discounts->MoveNext(); 
      }
      $sql = "SELECT products_id FROM " . TABLE_PRODUCTS; 
      $prodlist = $db->Execute($sql);
      while (!$prodlist->EOF) { 
         $prod = (int)$prodlist->fields['products_id']; 
         foreach ($global_xsells as $fld2) { 
             if ($prod != $fld2) { 
                $this->add_prod_to_prod($prod, $fld2, 'X', 0);
             }
         } 
         $prodlist->MoveNext();
      }
   }


Example 12: Using Big Chooser to do "Buy 4 of the same item, get 20% off the group" for all items in category 6

   function setup() {
      global $db; 
      $sql = "SELECT products_id FROM " . TABLE_PRODUCTS . " WHERE master_categories_id='6'";
      $prodlist = $db->Execute($sql);
      while (!$prodlist->EOF) { 
         $prod = (int)$prodlist->fields['products_id']; 
         $this->add_condition("Buy 4 " . zen_get_products_name($prod) . " get 20% off", true);
             $this->set_constraint(PROD, $prod, 4); 
             $this->set_cart_discount("%", 20, CART_DISCOUNT_CONSTRAINTS_ONLY);
         $prodlist->MoveNext();
      }
   }


Example 13: Making discounts contingent upon entering a coupon code

This topic is covered in my article Coupon Extensions for Zen Cart. See the section titled "Using Coupons with Big Spender, Big Chooser and Coupon Auto Add".

Example 14: Using Combination Discounts to do "Buy 2 Get 1 Free" for odd numbers of items, and "Buy 1 Get 1 50% off" for even numbers of items for category 34.

This is not exactly like the no_double_dip option of Big Chooser; this does limit the user to only one discount, but dynamically determines which discount is more appropriate based on cart quantity.


    function count_cats($count_cat) {
       $total = 0;
       if (!IS_ADMIN_FLAG) {
          $all_products = $_SESSION['cart']->get_products();
          foreach ($all_products as $product) {
              $cat = $product['category'];
              if (cat_compatible($cat, $count_cat)) {
                 $total += $product['quantity'];
              }
          }
       }
       return $total;
    }

   function setup() {
         if ($this->count_cats(34)%2 == 0) {
            // even number
            $this->add_linkage(CAT,34 ,1,CAT,34 ,1,"%", 50);
         } else { 
             // odd number
             $this->add_linkage(CAT,34 ,2,CAT,34 ,1,"%", 100);
         }
   }


The only issue with this code is that it doesn't interoperate well with the Combination Discounts Marketing Code, since it would only show one of these discounts on the product_info page. This is easy to fix: just change the setup() function as follows:
    function setup() {
       if (!IS_ADMIN_FLAG) {
         global $current_page_base; 
         $even = ($this->count_cats(34)%2 == 0); 
         $show_all = ($current_page_base == "product_info");
         if ($even || $show_all) {
            // even number
            $this->add_linkage(CAT,34 ,1,CAT,34 ,1,"%", 50);
         }
         if (!$even || $show_all) {
             // odd number
             $this->add_linkage(CAT,34 ,2,CAT,34 ,1,"%", 100);
         }
       }
     }


Likewise, a check for $current_page_base == "combo_promo" could be performed if the promotional page were being used.

Note that if Discount Preview is being used, the shopping cart sidebox must be turned off on the product info page (and the promo page) if a modification such as this is used, since we can't show all discounts and not use them in calculation too.

To turn off the shopping cart sidebox on the product info page, add this check to includes/modules/sideboxes/YOUR_TEMPLATE_NAME/shopping_cart.php:

Above the line
   if ($show_shopping_cart_box == true) {
add this block:
     if ($show_shopping_cart_box == true) { 
        if (($current_page_base == "shopping_cart") || 
            ($current_page_base  == "product_info") || 
            ($current_page_base  == "account_history_info")) { 
              $show_shopping_cart_box = false;
        }
     }



Example 15: Counting the number of items in the cart to prompt the user to add more and increase their BOGO savings.

This example is designed for use with BOGO Discount.

Create a new file called includes/functions/extra_functions/bogo.php with the following contents:
<?php
function check_bogo_message() {
   $all_products = $_SESSION['cart']->get_products();
   $count = 0; 
   foreach ($all_products as $product) { 
       $count += $product['quantity']; 
   }
   $rem = $count % 2; 
   if ($rem != 0) return true;
   return false;
}
and then create a file named includes/languages/english/extra_definitions/bogo.php with
;?php
define('BOGO_DISCOUNT_STRING', 'Buy just 1 more, get the lowest priced item at half off!');
Then, where ever you want the message (shopping cart page, product info page, etc.), just modify the template to contain
<?php
  if (check_bogo_message()) echo BOGO_DISCOUNT_STRING; 
?>
Of course there would be more logic required if your discount is Buy 2, get 1 free or something more complex.