Vendor Promotions - A (Free) Partial Implementation

Vendor promotions has been a requested feature for some time - see, for example, http://cscart.uservo…or-multi-vendor



However, CS Cart development has been too busy on other things to get around to it.



It turns out that it is quite easy to get a fairly workable partial implementation going - with minimal changes to core CS Cart code. I am sharing what I have done here in the hope that:[list]

[]Others may find it useful

[
]Those with development skills might check and extend what I have done

[*]CS Cart will eventually take over the code and incorporate it into the standard MV code

[/list]

My changes are based on MV 4.1.2



Limitations:



Gift certificates and coupons are not supported. Currently these are tied to the store rather than the vendor. I have not attempted to get them working.



Core code changes:



app/functions/fn.promotions.php - 3 lines changed. In function fn_promotion_apply $promotions is no longer static and $promotions are fetched on every function call



app/schemas/permissions/vendor_multivendor.php - 2 lines changed. Enables the Marketing|Promotions menu for vendors (thanks TBirnseth!)



app/schemas/promotions/schema.php - a number of lines commented out to removed unsupported promotion condition and bonus options. For example, gift certificates and coupons are not supported.



design/backend/templates/views/promotions/manage.tpl - This is optional. I chose to disable vendor catalog promotions because I think they are confusing for my vendors. Vendors can achieve the same effect simply by modifying product prices in the usual way. However, although I haven’t tested them as much, I think that catalog promotions do seem to work for vendors. If you want to allow them, you do not need to apply this core change. (I have uploaded this as manage.tpl.php because it wouldn’t let me upload a .tpl file - you will need to rename it)



Other changes:



All the other changes are in external add on code which I have placed under app/addons/my_changes. These changes are:



app/addons/my_changes/controllers/backend/promotions.pre.php is needed so that vendor promotions are associated with the vendor id - rather than being global “store” promotions



app/addons/my_changes/init.php needs to register the following hooks:


<br />
fn_register_hooks(<br />
	'calculate_cart_taxes_post',<br />
	'get_promotions',<br />
   ...<br />

```<br />
<br />
[u][b][u][b]app/addons/my_changes/[/b][/u]func.php[/b][/u] needs to implement the hook code. This is the major amount of code - but it is all quite straight forward. I have really just copied and pasted the standard CS Cart promotion code so that it is applied to vendor sub orders.<br />
Here are the hook functions:<br />
<br />
```php
<br />
/**<br />
* This hook is used in customer code to fetch promotions by company_id if<br />
* appropriate.<br />
*/<br />
function fn_my_changes_get_promotions(<br />
	&$params, &$fields, &$sortings, &$condition, &$join)<br />
{<br />
	if (AREA == 'C') {<br />
		//Customer case. This is called when...<br />
		//   a) calculating promotions by vendor. ie group.company_id is defined<br />
		//   <img src="upload://koLv6Nwy6blpxWjsJR7cOIcv8Nz.png" class="bbc_emoticon" alt="B)"> calculating global promotions only. We do not want to get vendor<br />
		//	  promotions as well so we need to explicitly specify<br />
		//	  company_id = 0.<br />
		$company_id = Registry::get('group.company_id');<br />
		if (empty($company_id)) {<br />
			if (strpos($condition, ':promotions.promotion_id') !== false) {<br />
				//If promotions are being retrieved by promotion_id we don't<br />
				//need to append a new condition, leave company_id empty.<br />
			} else {<br />
				//Set it to an explicit version of 0 that won't get filtered out<br />
				//by the "if (!$company_id)" code in fn_get_company_condition.<br />
				//That means that will add a ?:promotions.company_id = 0 to the<br />
				//SQL and return only global promotions - not vendor specific ones.<br />
				$company_id = '00';<br />
			}<br />
		}<br />
		$condition .= fn_get_company_condition(<br />
			'?:promotions.company_id', true, $company_id, false, true);<br />
	}<br />
}<br />
<br />
/**<br />
* Hooks into fn_calculate_cart_content of fn.cart.php which is called by<br />
* checkout.php.<br />
* <p/><br />
* This hook is placed after product groups have been established and global<br />
* promotions have been applied.<br />
* We use the product groups as promotion groups, iterating through each<br />
* group applying promotions for a given vendor and that vendor's products.<br />
*/<br />
function fn_my_changes_calculate_cart_taxes_post(<br />
	&$cart, &$cart_products, &$shipping_rates, &$calculate_taxes, &$auth)<br />
{<br />
<br />
	// Apply cart promotions<br />
	$cart['subtotal_discount'] = 0;<br />
<br />
	$product_groups = $cart['product_groups'];<br />
<br />
	foreach ($product_groups as $key_group => $group) {<br />
		$company_id = $group['company_id'];<br />
		Registry::set('group.company_id', $company_id);<br />
<br />
		//Compute group totals.<br />
		$amount_totals = array();<br />
		if (Registry::get('settings.General.disregard_options_for_discounts') == 'Y') {<br />
			foreach ($group['products'] as $k => $v) {<br />
				if (!empty($amount_totals[$v['product_id']])) {<br />
					$amount_totals[$v['product_id']] += $v['amount'];<br />
				} else {<br />
					$amount_totals[$v['product_id']] = $v['amount'];<br />
				}<br />
			}<br />
		}<br />
<br />
		$group_products = array();<br />
		foreach ($group['products'] as $k => $v) {<br />
			$group['products'][$k]['amount_total'] = isset($amount_totals[$v['product_id']]) ? $amount_totals[$v['product_id']] : $v['amount'];<br />
<br />
			$_cproduct = fn_get_cart_product_data($k, $group['products'][$k], false, $group, $auth);<br />
			if (empty($_cproduct)) { // FIXME - for deleted products for OM<br />
				unset($group['products'][$k]);<br />
				continue;<br />
			}<br />
<br />
			$group_products[$k] = $_cproduct;<br />
		}<br />
<br />
		//If there is anything in the group, apply the promotion rules to it.<br />
		if ($group['subtotal'] >= 0) {<br />
			//Accumulate promotions<br />
			$applied_promotions = fn_promotion_apply('cart', $group, $auth, $group_products);<br />
<br />
			if (!empty($applied_promotions)) {<br />
				if (empty($cart['applied_promotions'])) {<br />
					$cart['applied_promotions'] = $applied_promotions;<br />
				} else {<br />
					$cart['applied_promotions'] += $applied_promotions;<br />
				}<br />
			}<br />
<br />
			if (!empty($group['promotions'])) {<br />
				if (empty($cart['promotions'])) {<br />
					$cart['promotions'] = $group['promotions'];<br />
				} else {<br />
					$cart['promotions'] += $group['promotions'];<br />
				}<br />
			}<br />
<br />
			if (!$group['no_promotions']) {<br />
				$cart['no_promotions'] = false;<br />
			}<br />
<br />
			//Need to sum the group promotion discounts in the overall cart discount<br />
			$cart['subtotal_discount'] += $group['subtotal_discount'];<br />
		}<br />
	}<br />
<br />
	Registry::del('group.company_id');<br />
	fn_check_promotion_notices();<br />
<br />
}<br />

```<p><a href="127.0.0.1/uploads/monthly_02_2014/post-20481-0-47544900-1392774901.ipb">fn.promotions.php</a></p><p><a href="127.0.0.1/uploads/monthly_02_2014/post-20481-0-62958900-1392775185.ipb">vendor_multivendor.php</a></p><p><a href="127.0.0.1/uploads/monthly_02_2014/post-20481-0-60934600-1392775375.ipb">schema.php</a></p><p><a href="127.0.0.1/uploads/monthly_02_2014/post-20481-0-72785300-1392775955.ipb">manage.tpl.php</a></p><p><a href="127.0.0.1/uploads/monthly_02_2014/post-20481-0-79031300-1392776970.ipb">promotions.pre.php</a></p>

Added support for free shipping as a vendor promotion bonus.



Unfortunately I couldn’t use any existing fn.cart.php hook - none were suitable for supporting free shipping. I had to add a new hook (calculate_shipping_pre) which meant a single line modification to the core fn.cart.php code.



No other new changed files were introduced.



schema.php changed again to enable free shipping.



Here is the updated init.php extract.


fn_register_hooks(<br />
	'calculate_shipping_pre',<br />
	'get_promotions',<br />
...
```<br />
<br />
Here is the updated hook code:<br />
<br />
```php
/**<br />
* This hook is used in customer code to fetch promotions by company_id if<br />
* appropriate.<br />
*/<br />
function fn_my_changes_get_promotions(<br />
	&$params, &$fields, &$sortings, &$condition, &$join)<br />
{<br />
	if (AREA == 'C') {<br />
		//Customer case. This is called when...<br />
		//   a) calculating promotions by vendor. ie group.company_id is defined<br />
		//   <img src="upload://koLv6Nwy6blpxWjsJR7cOIcv8Nz.png" class="bbc_emoticon" alt="B)"> calculating global promotions only. We do not want to get vendor<br />
		//	  promotions as well so we need to explicitly specify<br />
		//	  company_id = 0.<br />
		$company_id = Registry::get('group.company_id');<br />
		if (empty($company_id)) {<br />
			if (strpos($condition, ':promotions.promotion_id') !== false) {<br />
				//If promotions are being retrieved by promotion_id we don't<br />
				//need to append a new condition, leave company_id empty.<br />
			} else {<br />
				//Set it to an explicit version of 0 that won't get filtered out<br />
				//by the "if (!$company_id)" code in fn_get_company_condition.<br />
				//That means that will add a ?:promotions.company_id = 0 to the<br />
				//SQL and return only global promotions - not vendor specific ones.<br />
				$company_id = '00';<br />
			}<br />
		}<br />
		$condition .= fn_get_company_condition(<br />
			'?:promotions.company_id', true, $company_id, false, true);<br />
	}<br />
}<br />
/**<br />
* Hooks into fn_calculate_cart_content of fn.cart.php which is called by<br />
* checkout.php.<br />
* <p/><br />
* This hook is located after global promotions have been applied.<br />
* We use the shipping product groups as promotion groups, iterating through<br />
* each group applying promotions for a given vendor and that vendor's products.<br />
*/<br />
function fn_my_changes_calculate_shipping_pre(<br />
	&$cart, &$cart_products, &$auth)<br />
{<br />
	$cart['subtotal_discount'] = 0;<br />
	// Apply vendor promotions based on shipping groups.<br />
	$location = fn_get_customer_location($auth, $cart);<br />
	$product_groups = Shippings::groupProductsList($cart_products, $location);<br />
	foreach ($product_groups as $key_group => $group) {<br />
		$company_id = $group['company_id'];<br />
		Registry::set('group.company_id', $company_id);<br />
		//Compute group totals.<br />
		$amount_totals = array();<br />
		if (Registry::get('settings.General.disregard_options_for_discounts') == 'Y') {<br />
			foreach ($group['products'] as $k => $v) {<br />
				if (!empty($amount_totals[$v['product_id']])) {<br />
					$amount_totals[$v['product_id']] += $v['amount'];<br />
				} else {<br />
					$amount_totals[$v['product_id']] = $v['amount'];<br />
				}<br />
			}<br />
		}<br />
		$group_products = array();<br />
		foreach ($group['products'] as $k => $v) {<br />
			$group['products'][$k]['amount_total'] = isset($amount_totals[$v['product_id']]) ? $amount_totals[$v['product_id']] : $v['amount'];<br />
			$_cproduct = fn_get_cart_product_data($k, $group['products'][$k], false, $group, $auth);<br />
			if (empty($_cproduct)) { // FIXME - for deleted products for OM<br />
				unset($group['products'][$k]);<br />
				continue;<br />
			}<br />
			$group_products[$k] = $_cproduct;<br />
		}<br />
		//If there is anything in the group, apply the promotion rules to it.<br />
		if ($group['subtotal'] >= 0) {<br />
			//Accumulate promotions<br />
			$applied_promotions = fn_promotion_apply('cart', $group, $auth, $group_products);<br />
			if (!empty($applied_promotions)) {<br />
				if (empty($cart['applied_promotions'])) {<br />
					$cart['applied_promotions'] = $applied_promotions;<br />
				} else {<br />
					$cart['applied_promotions'] += $applied_promotions;<br />
				}<br />
			}<br />
			if (!empty($group['promotions'])) {<br />
				if (empty($cart['promotions'])) {<br />
					$cart['promotions'] = $group['promotions'];<br />
				} else {<br />
					$cart['promotions'] += $group['promotions'];<br />
				}<br />
			}<br />
			if (!$group['no_promotions']) {<br />
				$cart['no_promotions'] = false;<br />
			}<br />
			//Keep track of any free shippings<br />
			if (!empty($group['free_shipping'])) {<br />
				if (empty($cart['free_shipping'])) {<br />
					$cart['free_shipping'] = $group['free_shipping'];<br />
				} else {<br />
					$cart['free_shipping'] += $group['free_shipping'];<br />
				}<br />
			}<br />
			//Need to sum the group promotion discounts in the overall cart discount<br />
			$cart['subtotal_discount'] += $group['subtotal_discount'];<br />
		}<br />
	}<br />
	Registry::del('group.company_id');<br />
	fn_check_promotion_notices();<br />
}<br />

```<p><a href="127.0.0.1/uploads/monthly_02_2014/post-20481-0-48256900-1393298657.ipb">fn.cart.php</a></p><p><a href="127.0.0.1/uploads/monthly_02_2014/post-20481-0-13496100-1393298680.ipb">schema.php</a></p>

This sounds really great! i will try asap. Are you running this code on your MV store?



Thank a lot in advance for sharing.

Yes - I am using it on my live store and it works fine.



It is now running on the latest version MV4.1.5

Any way to get this updated to 4.2.3 MV?

I don't have the files from 4.1.2 to compare to.

Thanks

This is now running on the latest version of MV - 4.3.4.

I don't have a 4.2.3 version - I skipped that version.

Any way to get this updated to 4.2.3 MV?

I don't have the files from 4.1.2 to compare to.

Thanks

Just try it, it should work. Do not forget to add the hook (fn_set_hook('calculate_shipping_pre'...) to appropriate line in the standard fn.cart.php

Any way to get this updated to 4.2.3 MV?

I don't have the files from 4.1.2 to compare to.

Thanks

Note that you can always contact CS-Cart support team and they will provide you with the distributive of the required version so that you can compare the files and transfer the modification correctly.

This is now running on the latest version of MV - 4.3.4.

I don't have a 4.2.3 version - I skipped that version.

Hi, do you have the updated files for 4.3.4

We got this version running now.

Thank you!

When I first posted this I did not expect to still be using it 2 years later! I had hoped that CSCart might have implemented vendor promotions by now.

I don't want to be in the position of supporting a "product". I just wanted to show other developers how it can be done.

If you are not a developer, I suggest that you contact Simcart, eComLabs or EZ Merchant Solutions and pay them to get it going for you. They should not charge you much since the changes that I have described are simple. That way you will get proper support - which I cannot offer.