As per the answer from here I have created an event for salesrule_validator_process and the code is below.

class CustomCartPriceRules implements ObserverInterface {
    public function __construct() {


     * @param \Magento\Framework\Event\Observer $observer
     * @return void
    public function execute(\Magento\Framework\Event\Observer $observer) {

        $result = $observer->getEvent()->getResult();
        $item = $observer->getEvent()->getItem();
        $rule = $observer->getEvent()->getRule();
        $address = $observer->getEvent()->getAddress();
        $qty = $item->getQty();

        if ($rule->getData('name') == 'Buy 2 get 1 free'):

                // overriding magento rules here and updating Almusbah Offer


            $products = [];
            $z = 0;
            $totalQty = 0;
            $dA = 0;
            foreach ($address->getAllItems() as $q):

                $validate = $rule->getActions()->validate($q);

                if ($validate):
                    $products[$z]['sku'] = $q->getData('sku');
                    $products[$z]['qty'] = $q->getData('qty');
                    $totalQty += $q->getData('qty');
                    $products[$z]['totalQty'] = $totalQty;
                    $freeQty = (int) ($totalQty / 3);
                    $products[$z]['freeQty'] = $freeQty;
                    if ($products[$z]['freeQty'] > 0):
                        $products[$z]['applyDiscount'] = 1;
                        $dicountPercent = 100;
                        $dA = ($products[$z]['freeQty'] * $q->getData('calculation_price'));
                        $products[$z]['discountAmount'] = $dA;
                        $totalQty -= 3*$products[$z]['freeQty']; // appying on each 3 items
            //echo '<pre>';print_r($products);exit;
            $chkCurrentSku = $item->getSku();

            foreach ($products as $p):
                if(isset($p['applyDiscount']) && !empty($p['applyDiscount']) && $chkCurrentSku == $p['sku']):
                    $discountAmount = $p['discountAmount'];
                    $fullDiscount = 100;



    public function almusbahBuy2Get1Offer($discountAmount, $result) {



My admin configuration is as follows.

enter image description here

When I add 2 items to the product with product A 4 qty and B with 2 qty.Where B is cheaper than A.So B should be free for 4 products of A.Instead one product from A and one from B are added into discount.

See the screenshot below.

enter image description here

In this case products with price 14 and qty 2 should be added as discounted instead of 1 from 14 and another of 145.

Can someone please help how I can add the cheaper product as discounted in the above code?

1 Answer 1


Your calculation discounts always the third item without regarding the price. In order to dicount the cheapest items you need to sort the item collection by the calculation_price, calculate the total items to be discounted and then discount the items beginning with the cheapest order item.

A code like the following should work. But please keep in mind, I haven't tested it!

//first the total free quantity needs to be calculated based on all items in the quote
$quote = $observer->getEvent()->getQuote();
$totalFreeQty = (int) ($quote->getItemsSummaryQty() / 3);

//second you need all quote items sorted by calculation_price ascending; 
//that won't work with collection sorting, therefore you need to use the items array
//to make sorting easy we use the price as key
$allItems = $quote->getAllVisibleItems();
$sortedItems = [];
foreach ($allItems as $quoteItem) {
    //convert the float price into integer preserving all decimal positions
    $priceKey = (int) ($quoteItem->getData('calculation_price') * 1000);
    //if item qith the same price already exists, we increment until we find a free key
    //in this way we make sure that no key is overwritten and we keep the price sort order
    //in case that two items have the same price the first item of the collection will be sorted first
    while (isset($sortedItems[$priceKey])){
    $sortedItems[$priceKey] = $quoteItem;

//now you can use your foreach loop as follows (I have changed only the freeQty calculation)

foreach ($sortedItems as $q):
    $validate = $rule->getActions()->validate($q);

    if ($validate):
        $products[$z]['sku'] = $q->getData('sku');
        $products[$z]['qty'] = $q->getData('qty');
        $products[$z]['freeQty'] = 0;
        if ($totalFreeQty > 0):
            if ($totalFreeQty > $products[$z]['qty']){
                $products[$z]['freeQty'] = $products[$z]['qty'];
                $totalFreeQty = $totalFreeQty - $products[$z]['qty'];
            } else {
                $products[$z]['freeQty'] = $totalFreeQty;
                $totalFreeQty = 0
            $products[$z]['applyDiscount'] = 1;
            $dicountPercent = 100;
            $dA = ($products[$z]['freeQty'] * $q->getData('calculation_price'));
            $products[$z]['discountAmount'] = $dA;
  • Thanks. That worked like charm
    – Melvin
    Commented Mar 28, 2019 at 9:11
  • one more thing . if i want to give 50% off to the cheapest product once the condition is matched how can i achieve it . currently it is 100%
    – Melvin
    Commented Mar 28, 2019 at 9:49
  • 1
    Something like $products[$z]['discountAmount'] = $dA * 0,5; and in the later loop $item->setDiscountPercent(50); should work.
    – HelgeB
    Commented Mar 28, 2019 at 11:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.