Reading list Switch to dark mode

    How to create a custom cart rule for Buy X Get next Y with percent discount in Magento 2

    Updated 7 February 2024

    Hello Friends, 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, 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

    Searching for an experienced
    Magento 2 Company ?
    Find out More
    <?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.

    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