Magento 2 - discount depend on Payment Method does not work

This rule doesn't work because Magento 2 doesn't save payment method to quote when you select one. And it also doesn't reload totals when selecting a payment method. And unfortunately, you have to write a custom module to solve the issue.

The new module needs only 4 files to be created:

  1. app/code/Namespace/ModuleName/etc/frontend/routes.xml

    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
        <router id="standard">
            <route id="namespace_modulename" frontName="namespace_modulename">
                <module name="Namespace_ModuleName"/>
            </route>
        </router>
    </config>
    

    This will define a new controller for our module.

  2. app/code/Namespace/ModuleName/Controller/Checkout/ApplyPaymentMethod.php

    <?php
    
    namespace Namespace\ModuleName\Controller\Checkout;
    
    class ApplyPaymentMethod extends \Magento\Framework\App\Action\Action
    {
        /**
         * @var \Magento\Framework\Controller\Result\ForwardFactory
         */
        protected $resultForwardFactory;
    
        /**
         * @var \Magento\Framework\View\LayoutFactory
         */
        protected $layoutFactory;
    
        /**
         * @var \Magento\Checkout\Model\Cart
         */
        protected $cart;
    
        /**
         * @param \Magento\Framework\App\Action\Context $context
         * @param \Magento\Framework\View\LayoutFactory $layoutFactory
         * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory
         */
        public function __construct(
            \Magento\Framework\App\Action\Context $context,
            \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory,
            \Magento\Framework\View\LayoutFactory $layoutFactory,
            \Magento\Checkout\Model\Cart $cart
        ) {
            $this->resultForwardFactory = $resultForwardFactory;
            $this->layoutFactory = $layoutFactory;
            $this->cart = $cart;
    
            parent::__construct($context);
        }
    
        /**
         * @return \Magento\Framework\Controller\ResultInterface
         */
        public function execute()
        {
            $pMethod = $this->getRequest()->getParam('payment_method');
    
            $quote = $this->cart->getQuote();
    
            $quote->getPayment()->setMethod($pMethod['method']);
    
            $quote->setTotalsCollectedFlag(false);
            $quote->collectTotals();
            $quote->save();
        }
    }
    

    This file creates controller action to save the selected payment method to quote

  3. app/code/Namespace/ModuleName/view/frontend/requirejs-config.js

    var config = {
        map: {
            '*': {
                'Magento_Checkout/js/action/select-payment-method':
                    'Namespace_ModuleName/js/action/select-payment-method'
            }
        }
    };
    

    This file allows to override Magento_Checkout/js/action/select-payment-method file

  4. app/code/Namespace/ModuleName/view/frontend/web/js/action/select-payment-method.js

    define(
        [
            'Magento_Checkout/js/model/quote',
            'Magento_Checkout/js/model/full-screen-loader',
            'jquery',
            'Magento_Checkout/js/action/get-totals',
        ],
        function (quote, fullScreenLoader, jQuery, getTotalsAction) {
            'use strict';
            return function (paymentMethod) {
                quote.paymentMethod(paymentMethod);
    
                fullScreenLoader.startLoader();
    
                jQuery.ajax('/namespace_modulename/checkout/applyPaymentMethod', {
                    data: {payment_method: paymentMethod},
                    complete: function () {
                        getTotalsAction([]);
                        fullScreenLoader.stopLoader();
                    }
                });
    
            }
        }
    );
    

    Sends ajax request to save payment method and reload cart totals.

P.S. Parts of the code were taken from Payment Fee extension for Magento 2.


On Magento 2.2 i couldn't get MagestyApps answer to work. I needed to add some additional files. Because:

  • Cart Price Rule for Payment Method was removed from admin (as pointed out by DaFunkyAlex);
  • In Magento 2.2 the discount was not being applied on the quote, because the method \Magento\AdvancedSalesRule\Model\Rule\Condition\FilterTextGenerator\Address\PaymentMethod::generateFilterText (actually it falls back to \Magento\AdvancedSalesRule\Model\Rule\Condition\FilterTextGenerator\Address::generateFilterText), was expecting the data payment_method to be set on the quote addresses;
  • Even changing the controller from MagestyApps answer to set payment_method data on the quote addresses, didn't work when the quote became an order, because it don't persist payment_method;

The following module worked out for me (thanks to MagestyApps answer, it was based on top of that):

registration.php

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_SalesRulesPaymentMethod',
    __DIR__
);

etc/module.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_SalesRulesPaymentMethod" setup_version="1.0.0">
        <sequence>
            <module name="Magento_AdvancedSalesRule"/>
            <module name="Magento_Checkout"/>
            <module name="Magento_SalesRules"/>
            <module name="Magento_Quote"/>
        </sequence>
    </module>
</config>

etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\AdvancedSalesRule\Model\Rule\Condition\FilterTextGenerator\Address\PaymentMethod"
                type="Vendor\SalesRulesPaymentMethod\Model\Rule\Condition\FilterTextGenerator\Address\PaymentMethod"/>
    <type name="Magento\SalesRule\Model\Rule\Condition\Address">
        <plugin name="addPaymentMethodOptionBack" type="Vendor\SalesRulesPaymentMethod\Plugin\AddPaymentMethodOptionBack" />
    </type>
</config>

etc/frontend/routes.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="salesrulespaymentmethod" frontName="salesrulespaymentmethod">
            <module name="Vendor_SalesRulesPaymentMethod"/>
        </route>
    </router>
</config>

Controller/Checkout/ApplyPaymentMethod.php

<?php

namespace Vendor\SalesRulesPaymentMethod\Controller\Checkout;

use Magento\Checkout\Model\Cart;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Controller\Result\ForwardFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\View\LayoutFactory;
use Magento\Quote\Model\Quote;

class ApplyPaymentMethod extends Action
{
    /**
     * @var ForwardFactory
     */
    protected $resultForwardFactory;

    /**
     * @var LayoutFactory
     */
    protected $layoutFactory;

    /**
     * @var Cart
     */
    protected $cart;

    /**
     * @param Context $context
     * @param LayoutFactory $layoutFactory
     * @param ForwardFactory $resultForwardFactory
     */
    public function __construct(
        Context $context,
        ForwardFactory $resultForwardFactory,
        LayoutFactory $layoutFactory,
        Cart $cart
    ) {
        $this->resultForwardFactory = $resultForwardFactory;
        $this->layoutFactory = $layoutFactory;
        $this->cart = $cart;

        parent::__construct($context);
    }

    /**
     * @return ResponseInterface|ResultInterface|void
     * @throws \Exception
     */
    public function execute()
    {
        $pMethod = $this->getRequest()->getParam('payment_method');

        /** @var Quote $quote */
        $quote = $this->cart->getQuote();

        $quote->getPayment()->setMethod($pMethod['method']);

        $quote->setTotalsCollectedFlag(false);
        $quote->collectTotals();

        $quote->save();
    }
}

Model/Rule/Condition/FilterTextGenerator/Address/PaymentMethod.php

<?php

namespace Vendor\SalesRulesPaymentMethod\Model\Rule\Condition\FilterTextGenerator\Address;

use Magento\AdvancedSalesRule\Model\Rule\Condition\FilterTextGenerator\Address\PaymentMethod as BasePaymentMethod;

class PaymentMethod extends BasePaymentMethod
{
    /**
     * @param \Magento\Framework\DataObject $quoteAddress
     * @return string[]
     */
    public function generateFilterText(\Magento\Framework\DataObject $quoteAddress)
    {
        $filterText = [];
        if ($quoteAddress instanceof \Magento\Quote\Model\Quote\Address) {
            $value = $quoteAddress->getQuote()->getPayment()->getMethod();
            if (is_scalar($value)) {
                $filterText[] = $this->getFilterTextPrefix() . $this->attribute . ':' . $value;
            }
        }

        return $filterText;
    }
}

Plugin/AddPaymentMethodOptionBack.php

<?php

namespace Vendor\SalesRulesPaymentMethod\Plugin;

use Magento\SalesRule\Model\Rule\Condition\Address;

class AddPaymentMethodOptionBack
{
    /**
     * @param Address $subject
     * @param $result
     * @return Address
     */
    public function afterLoadAttributeOptions(Address $subject, $result)
    {
        $attributeOption = $subject->getAttributeOption();
        $attributeOption['payment_method'] = __('Payment Method');

        $subject->setAttributeOption($attributeOption);

        return $subject;
    }
}

view/frontend/requirejs-config.js

var config = {
    map: {
        '*': {
            'Magento_Checkout/js/action/select-payment-method':
                'Vendor_SalesRulesPaymentMethod/js/action/select-payment-method'
        }
    }
};

view/frontend/web/js/action/select-payment-method.js

define(
    [
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/full-screen-loader',
        'jquery',
        'Magento_Checkout/js/action/get-totals',
    ],
    function (quote, fullScreenLoader, jQuery, getTotalsAction) {
        'use strict';
        return function (paymentMethod) {
            quote.paymentMethod(paymentMethod);

            fullScreenLoader.startLoader();

            jQuery.ajax('/salesrulespaymentmethod/checkout/applyPaymentMethod', {
                data: {payment_method: paymentMethod},
                complete: function () {
                    getTotalsAction([]);
                    fullScreenLoader.stopLoader();
                }
            });

        }
    }
);

We just checked the same rule and found that it doesn't work. Using the same conditions, no information about selected chosen method is sent and it is not recorded anywhere until the order is placed and the rule may not work.

enter image description here

Address has no payment method until validation and it gets the payment method from payment quote that doesn't exist because no information has been sent ($model->getQuote()->getPayment()->getMethod() returns null).

enter image description here

We suppose, that this is Magento bug. When you choose a payment method the information should be sent in advance.