Webkul
Services
Technologies Bagisto QloApps
iOS Android SAP BigCommerce Wix
Trusted by
Samsung Intel Toyota Walmart Disney Nokia Nestlé Decathlon
See all customers
Back to Top
DEV

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.

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