How to create custom cart rule for Buy X Get next Y in Magento 2
For an e-commerce business to flourish, it is quite necessary to inculcate effective marketing strategies that lay a strong influence on the customers.
Having said this, Magento 2 Special Promotions and discounts are one of the most renowned marketing strategies.
As you know Magento 2 provides us with four types of cart rules.
In this blog, We will learn how can we add a custom cart rule (Buy X Get next Y with percent discount)
Let’s start step by step.
Note:- You must have installed your custom Module on which you are going to implement this.
Step 1: Create file Webkul\CartRule\etc\adminhtml\di.xml
<?xml version="1.0"?>
<!--
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\SalesRule\Model\Rule\Metadata\ValueProvider">
<plugin name="salesrule-plugin" type="Webkul\CartRule\Plugin\Rule\Metadata\ValueProvider" sortOrder="1" />
</type>
</config>
Step 2: We need to create the ValueProvider plugin file which add a custom cart rule option in the cart rules dropdown. Webkul\CartRule\Plugin\Rule\Metadata\ValueProvider.php
<?php
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
namespace Webkul\CartRule\Plugin\Rule\Metadata;
class ValueProvider
{
public function afterGetMetadataValues(
\Magento\SalesRule\Model\Rule\Metadata\ValueProvider $subject,
$result
) {
$applyOptions = [
'label' => __('Custom'),
'value' => [
[
'label' => 'Buy X Get next Y with M% discount',
'value' => 'buy_x_get_next_y_with_percent',
]
],
];
array_push($result['actions']['children']['simple_action']['arguments']['data']['config']['options'], $applyOptions);
return $result;
}
}
After adding the above code to your custom module. Deploy the code and see the result.
Step 3: Create Webkul/CartRule/view/adminhtml/ui_component/sales_rule_form.xml file to add custom fields in the Cart price rule form.
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
-->
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<fieldset name="actions" sortOrder="30">
<field name="custom_step_nqty" formElement="input">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="source" xsi:type="string">sales_rule</item>
<item name="sortOrder" xsi:type="number">4</item>
</item>
</argument>
<settings>
<validation>
<rule name="required-entry" xsi:type="boolean">true</rule>
<rule name="validate-number" xsi:type="boolean">true</rule>
<rule name="validate-zero-or-greater" xsi:type="boolean">true</rule>
</validation>
<dataType>text</dataType>
<label translate="true">Discount Qty (Buy Y)</label>
<dataScope>custom_step_nqty</dataScope>
</settings>
</field>
<field name="promo_skus" formElement="textarea">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="source" xsi:type="string">sales_rule</item>
<item name="notice" xsi:type="string" translate="true">Enter Y products separated by comma. Enter All SKU(s) if product is Customisable.</item>
</item>
</argument>
<settings>
<dataType>number</dataType>
<label translate="true">Promo SKU</label>
<dataScope>promo_skus</dataScope>
</settings>
</field>
</fieldset>
</form>
You will see the result in the cart price rule :
Discount Qty (Buy Y): Set quantity for Y products eligible for discount.
Promo SKU: This field defines the Y products, Enter Y products SKU separated by comma. Enter All SKU(s) if the product is Customisable.
Note:- Non-Promo products SKU are defined as X products.
Step 4: Create a layout file Webkul/CartRule/view/adminhtml/layout/sales_rule_promo_quote_edit.xml to add a template file.
<?xml version="1.0"?>
<!--
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="head.components">
<block class="Magento\Framework\View\Element\Js\Components" name="sales_custom_rule_form_page_head_components" template="Webkul_CartRule::promo/customrulejs.phtml"/>
</referenceBlock>
</body>
</page>
Step 5: Create a template Webkul/view/adminhtml/templates/promo/customrulejs.phtml file for managing the custom field’s appearance.
<?php
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
?>
<script>
require([
'jquery',
"uiRegistry"
], function($, registry) {
$(document).ready(function() {
$('body').on('click', ".fieldset-wrapper-title", function() {
$('body select[name=simple_action]').trigger('change');
});
$('body').on('change', 'select[name=simple_action]', function() {
$('body input[name=discount_amount]').parent().parent()
.find('label span').text("<?= /* @noEscape */ __('Discount Amount'); ?>");
if ($(this).val() == 'buy_x_get_next_y_with_percent') {
let discountAmount = Math.min(100, $('body input[name=discount_amount]').val());
discountAmount = discountAmount ? discountAmount : 1;
let customRuleElementNode = $('body select[name=simple_action] option[value="buy_x_get_next_y_with_percent"]');
let customRuleLabel = customRuleElementNode.text().replace('M%', `${discountAmount}%`);
customRuleElementNode.text(customRuleLabel);
$('body textarea[name=promo_skus]').parent().parent().show();
$('body input[name=discount_amount]').parent().parent().find('label span')
.text("<?= /* @noEscape */ __('Discount Amount (in %)'); ?>");
$('body input[name=custom_step_nqty]').parent().parent().show();
} else {
$('body input[name=custom_step_nqty]').parent().parent().hide();
$('body textarea[name=promo_skus]').parent().parent().hide();
}
});
});
});
</script>
Step 6: Configure declarative schema Webkul/CartRule/etc/db_schema.xml file to save custom field data of the Cart price rule form.
<?xml version="1.0"?>
<!--
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
-->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="salesrule" resource="default">
<column xsi:type="text" name="custom_step_nqty" nullable="true" comment="N Qty"/>
<column xsi:type="text" name="promo_skus" nullable="true" comment="Promotion SKUs"/>
</table>
</schema>
Step 7: For Calculation of the custom cart rule, create Webkul\CartRule\etc\di.xml file and pass the class as an argument in class Magento\SalesRule\Model\Rule\Action\Discount\Calculator
<?xml version="1.0"?>
<!--
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory">
<arguments>
<argument name="discountRules" xsi:type="array">
<item name="buy_x_get_next_y_with_percent" xsi:type="string">\Webkul\CartRule\Model\Rule\Action\Discount\BuyXGetNextYWithPercent</item>
</argument>
</arguments>
</type>
</config>
Step 8: Create class Webkul\CartRule\Model\Rule\Action\Discount\BuyXGetNextYWithPercent.php file which is passed as an argument in the above file.
<?php
/**
* Webkul Software.
*
* @category Webkul
* @package Webkul_CartRule
* @author Webkul
* @copyright Copyright (c) Webkul Software Private Limited (https://webkul.com)
* @license https://store.webkul.com/license.html
*/
namespace Webkul\CartRule\Model\Rule\Action\Discount;
use Magento\SalesRule\Model\Rule\Action\Discount\AbstractDiscount;
class BuyXGetNextYWithPercent extends AbstractDiscount
{
/**
* Calculate Buy X Get next Y with M% discount amount
*
* @param \Magento\SalesRule\Model\Rule $rule
* @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
* @param float $qty
* @return \Magento\SalesRule\Model\Rule\Action\Discount\Data
*/
public function calculate($rule, $item, $qty)
{
$rulePercent = min(100, $rule->getDiscountAmount());
$discountData = $this->_calculate($rule, $item, $qty, $rulePercent);
return $discountData;
}
/**
* @param \Magento\SalesRule\Model\Rule $rule
* @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
* @param float $qty
* @param float $rulePercent
* @return Data
*/
protected function _calculate($rule, $item, $qty, $rulePercent)
{
/** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */
$discountData = $this->discountFactory->create();
/**
* Calculation logic here
*/
$discountStep = $rule->getDiscountStep();
$maxDiscountQty = $rule->getDiscountQty();
$currentDiscountAmount = $rulePercent;
$qty = $item->getQty();
$cartSKUs = [];
$promoSkus = $rule->getpromoSkus();
$totalQuantity = $item->getQuote()->getItemsQty();
$yProducts = $rule->getData('custom_step_nqty');
$productPriceSKUWise = [];
$productQty = [];
foreach ($item->getQuote()->getAllVisibleItems() as $currentItem) {
if (!$this->validator->canApplyRules($currentItem)) {
continue;
}
$currentItemSku = $currentItem->getSku();
if ($currentItem->getProductType() == 'bundle') {
$currentItemSku = $this->getBundleProductSku($currentItem->getId(), $currentItemSku, $item);
}
$productQty[$currentItemSku] = $currentItem->getQty();
$cartSKUs[] = $currentItemSku;
$productPriceSKUWise[$currentItemSku]['price'] = $this->validator->getItemPrice($currentItem);
$productPriceSKUWise[$currentItemSku]['base_price'] = $this->validator->getItemBasePrice($currentItem);
}
$discountAmount = $this->getAmountSkuWise(
$currentDiscountAmount,
$qty,
$maxDiscountQty,
$discountStep,
$totalQuantity,
$yProducts,
$productQty,
$item,
$cartSKUs,
$promoSkus,
$productPriceSKUWise
);
/** Set the discount price in Price **/
$discountData->setAmount($discountAmount['discount']);
$discountData->setBaseAmount($discountAmount['base_discount']);
$discountData->setOriginalAmount($discountAmount['discount']);
$discountData->setBaseOriginalAmount($discountAmount['base_discount']);
return $discountData;
}
/**
* Bundle product SKu
*
* @param int $itemId
* @param string $currentItemSku
* @param \Magento\Quote\Model\Quote\Item $item
* @return string
*/
public function getBundleProductSku($itemId, $currentItemSku, $item)
{
foreach ($item->getQuote()->getAllItems() as $currentItem) {
if ($currentItem->getParentItemId() == $itemId) {
$currentItemSku = str_replace("-" . $currentItem->getSku(), "", $currentItemSku);
}
}
return $currentItemSku;
}
/**
* Get discount amount as per promotional SKU
*
* @param integer $currentDiscountAmount
* @param integer $qty
* @param integer $maxDiscountQty
* @param integer $discountStep
* @param integer $totalQuantity
* @param int $yProducts
* @param array $productQty
* @param \Magento\Quote\Model\Quote\Item $item
* @param array $cartSKUs
* @param string $promoSkus
* @param array $productPriceSKUWise
* @return array
*/
private function getAmountSkuWise(
$currentDiscountAmount,
$qty,
$maxDiscountQty,
$discountStep,
$totalQuantity,
$yProducts,
$productQty,
$item,
$cartSKUs,
$promoSkus,
$productPriceSKUWise
) {
$discountAmount = ['discount' => 0, 'base_discount' => 0];
if (empty($yProducts) || $yProducts == 0) {
$yProducts = 1;
}
$maxDiscountQty = $maxDiscountQty ? $maxDiscountQty : 1;
$currentItemSku = $item->getSku();
if ($item->getProductType() == 'bundle') {
$currentItemSku = $this->getBundleProductSku($item->getId(), $currentItemSku, $item);
}
$skus = explode(',', trim($promoSkus) ?? '');
if (!array_diff($cartSKUs, $skus)) {
return $discountAmount;
}
if (in_array($currentItemSku, $cartSKUs) && in_array($currentItemSku, $skus)) {
foreach ($skus as $sku) {
if (array_key_exists($sku, $productQty)) {
$productQtyitem = $productQty[$sku];
if ($item->getProduct()->getTypeId() == 'bundle') {
$promoSKUPrice = $this->getBundleProductPrice($sku, $item);
} else {
$promoSKUPrice = $this->getProductPrice(
$sku,
$productPriceSKUWise
);
}
$qty = $totalQuantity - $productQtyitem;
if (in_array($sku, $cartSKUs) && $discountStep <= $qty && $yProducts <= $productQtyitem) {
if ($currentItemSku == $sku) {
$nDiscountQty = min($qty, $productQtyitem, $maxDiscountQty);
$discountAmount['discount'] = $discountAmount['discount'] +
($promoSKUPrice['price'] * $currentDiscountAmount / 100) * $nDiscountQty;
$discountAmount['base_discount'] = $discountAmount['base_discount'] +
($promoSKUPrice['base_price'] * $currentDiscountAmount / 100) * $nDiscountQty;
}
}
}
}
}
return $discountAmount;
}
/**
* Bundle Product Price
*
* @param string $sku
* @param \Magento\Quote\Model\Quote\Item $item
* @return array
*/
public function getBundleProductPrice($sku, $item)
{
$price = 0;
foreach ($item->getQuote()->getAllItems() as $currentItem) {
if (
$currentItem->getProductType() == 'bundle'
&&
$currentItem->getProduct()->getData('sku') == $sku
) {
$price = $this->validator->getItemPrice($currentItem);;
$basePrice = $this->validator->getItemBasePrice($currentItem);
}
}
return ['price' => $price, 'base_price' => $basePrice];
}
/**
* Get Product Price
*
* @param string $sku
* @param array $productPriceSKUWise
* @return array
*/
public function getProductPrice($sku, $productPriceSKUWise)
{
$totalSkuPrice = 0;
if (isset($productPriceSKUWise[$sku])) {
$totalSkuPrice = $productPriceSKUWise[$sku];
}
return $totalSkuPrice;
}
}
The discount will be applicable only if the product, for which the admin defines the Promo SKU is present in the cart as shown in the image below.
SKU 24-MB01 is mentioned in the promo SKU so the cart considered this product as a Y product and 24-MB05 is a non-promo SKU so the cart considered this product as an X product.
You can also go through the best Magento 2 extensions catalog list.
Hope this will be helpful. Thanks 🙂