Back to Top

How to create custom cart rule for Buy X Get next Y in Magento 2

Updated 21 March 2024

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.

Searching for an experienced
Magento 2 Company ?
Find out More

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.

Screenshot-from-2024-02-07-19-14-15

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 :

Screenshot-2024-02-07T192850.744

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.

Screenshot-2024-02-07T203639.958
Screenshot-2024-02-07T204159.278

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 🙂

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

Back to Top

Message Sent!

If you have more details or questions, you can reply to the received confirmation email.

Back to Home