0

I want to add a custom fee to the cart summary when the checkbox is check and remove when unchecked and also adjusting the order total with each checkbox change. How can I achieve this? example order summary

1 Answer 1

0

I had to do the same but on the checkout summary. I hope this can help you. since my checkout is one step checkout because i'm using a template I have decided to create a new step

To have our values dynamically, I created the fields in the admin etc/adminhtml/system.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="config_bag_checkout" translate="label" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
            <class>separator-top</class>
            <label>Configuration</label>
            <tab>xxxxxxxx</tab>
            <resource>xxxxxxxx::erp</resource>
            <group id="customfee" translate="label" type="text" sortOrder="500" showInDefault="1" showInWebsite="1"
                   showInStore="1">
                <label>Bag Fee Settings</label>
                <field id="status" translate="label comment" type="select" sortOrder="1" showInDefault="1"
                       showInWebsite="1" showInStore="1">
                    <label>Status</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="bagfee_amount" translate="label comment" type="text" sortOrder="3" showInDefault="1"
                       showInWebsite="1" showInStore="1">
                    <label>Bag Fee Amount</label>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
                <field id="minimum_order_amount" translate="label comment" type="text" sortOrder="4" showInDefault="1"
                       showInWebsite="1" showInStore="1">
                    <label>Minimum Order Amount</label>
                    <comment><![CDATA[Greaterthan or equal to]]></comment>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
            </group>
        </section>
    </system>
</config>

Then create a helper to get the data from the admin Helper/Data.php

<?php

namespace Vendor\Module\Helper;

use Magento\Framework\App\Helper\AbstractHelper;

class Data extends AbstractHelper {
    /**
     * Bag fee config path
     */
    const CONFIG_CUSTOM_IS_ENABLED = 'config_bag_checkout/customfee/status';
    const CONFIG_CUSTOM_FEE = 'config_bag_checkout/customfee/bagfee_amount';
    const CONFIG_MINIMUM_ORDER_AMOUNT = 'config_bag_checkout/customfee/minimum_order_amount';

    /**
     * @return mixed
     */
    public function isModuleEnabled() {
        $storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
        $isEnabled = $this->scopeConfig->getValue(self::CONFIG_CUSTOM_IS_ENABLED, $storeScope);
        return $isEnabled;
    }

    /**
     * Get bag fee
     *
     * @return mixed
     */
    public function getBagFee() {
        $storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
        $fee = $this->scopeConfig->getValue(self::CONFIG_CUSTOM_FEE, $storeScope);
        return $fee;
    }

    /**
     * @return mixed
     */
    public function getMinimumOrderAmount() {
        $storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
        $MinimumOrderAmount = $this->scopeConfig->getValue(self::CONFIG_MINIMUM_ORDER_AMOUNT, $storeScope);
        return $MinimumOrderAmount;
    }
}

To trigger the class that is in charge of updating the prices we must create a sales.xml file inside etc folder

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd">
    <section name="quote">
        <group name="totals">
            <item name="customfee" instance="Vendor\Module\Model\Total\Customfee" sort_order="150"/>
        </group>  
    </section>
</config>

Then create the file Model/Total/Customfee.php

<?php
namespace Vendor\Module\Model\Total;

class Customfee extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal {
    /**
     * @var \Vendor\Module\Helper\Data
     */
    protected $dataHelper;

   /**
     * Collect grand total address amount
     *
     * @param \Magento\Quote\Model\Quote $quote
     * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     * @return $this
     */
    protected $quoteValidator = null; 

    public function __construct(
        \Magento\Quote\Model\QuoteValidator $quoteValidator,
        \Vendor\Module\Helper\Data $dataHelper
    ) {
        $this->dataHelper = $dataHelper;
        $this->quoteValidator = $quoteValidator;
    }

    public function collect(
        \Magento\Quote\Model\Quote $quote,
        \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment,
        \Magento\Quote\Model\Quote\Address\Total $total
    ) {
        parent::collect($quote, $shippingAssignment, $total);

        $data = [];
        $bssData = $quote->getBssCustomfield();
        if ($bssData) {
            $data = json_decode($bssData);
        }
        if (isset($data->bag_option) && $data->bag_option === "true") {
            $bagPrice = $this->dataHelper->getBagFee();
            $minimumOrderAmount = $this->dataHelper->getMinimumOrderAmount();
            $bagFee = $total->getBaseSubtotal() <= $minimumOrderAmount ? $this->dataHelper->getBagFee(): 0;

            $total->setTotalAmount('customfee', $bagFee);
            $total->setBaseTotalAmount('customfee', $bagFee);
    
            $total->setCustomFee($bagFee);
    
            $total->setGrandTotal($total->getGrandTotal());
            $total->setBaseGrandTotal($total->getBaseGrandTotal());
        } else {
            $bagFee = 0;
            $total->setTotalAmount('customfee', $bagFee);
            $total->setBaseTotalAmount('customfee', $bagFee);
    
            $total->setCustomFee($bagFee);
    
            $total->setGrandTotal($total->getGrandTotal());
            $total->setBaseGrandTotal($total->getBaseGrandTotal());
        }

        return $this;
    } 

    protected function clearValues(Address\Total $total) {
        $total->setTotalAmount('subtotal', 0);
        $total->setBaseTotalAmount('subtotal', 0);
        $total->setTotalAmount('tax', 0);
        $total->setBaseTotalAmount('tax', 0);
        $total->setTotalAmount('discount_tax_compensation', 0);
        $total->setBaseTotalAmount('discount_tax_compensation', 0);
        $total->setTotalAmount('shipping_discount_tax_compensation', 0);
        $total->setBaseTotalAmount('shipping_discount_tax_compensation', 0);
        $total->setSubtotalInclTax(0);
        $total->setBaseSubtotalInclTax(0);
    }
    /**
     * @param \Magento\Quote\Model\Quote $quote
     * @param Address\Total $total
     * @return array|null
     */
    /**
     * Assign subtotal amount and label to address object
     *
     * @param \Magento\Quote\Model\Quote $quote
     * @param Address\Total $total
     * @return array
     */
    public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total) {
        return [
            'code' => 'customfee',
            'title' => 'Custom Fee',
            'value' => $this->dataHelper->getBagFee(),
        ];
    }

    /**
     * Get Subtotal label
     *
     * @return \Magento\Framework\Phrase
     */
    public function getLabel() {
        return __('Custom Fee');
    }
}

In my case I have installed a bss module called Checkout Custom Field this is how I validate if the user has already checked the box or not but you can replace this to use a checkoutSession instead of my implementation. any will work, the important thing is to know when the box is checked or unchecked to apply the fee.

Now we need to create a controller, this is called when the user selects the checkbox to save the value.

etc/frontend/routes.xml

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

Controller/Index/Index.php

<?php
namespace Vendor\MOdule\Controller\Index;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Quote\Model\QuoteIdMaskFactory;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Framework\Controller\Result\JsonFactory;

class Index extends Action {
    private     $quoteIdMaskFactory;

    /** @var \Magento\Framework\Controller\Result\Json $result */
    protected   $resultJsonFactory;

    /**
     * @var CartRepositoryInterface
     */
    protected $cartRepository;

    public function __construct(
        Context  $context,
        JsonFactory $resultJsonFactory,
        QuoteIdMaskFactory $quoteIdMaskFactory,
        CartRepositoryInterface $cartRepository
    ) {
        $this->cartRepository = $cartRepository;
        $this->resultJsonFactory = $resultJsonFactory;
        $this->quoteIdMaskFactory = $quoteIdMaskFactory;
        parent::__construct($context);
    }   


    public function execute() {
        $result = $this->resultJsonFactory->create();
        $resultRedirect = $this->resultRedirectFactory->create();

        $post = $this->getRequest()->getPostValue();
        $resultRedirect->setPath('checkout/cart');
        if (!$post) {
            return $resultRedirect;
        }

        try {
            $maskedQuoteId = $post['quote_id'];
            $customFeeCheck = $post['enable_custom_fee'];
            $quoteIdMask = $this->quoteIdMaskFactory->create()->load($maskedQuoteId, 'masked_id');
            $quoteId = $quoteIdMask->getQuoteId();

            $quote = $this->cartRepository->get($quoteId);
            $quote->setBssCustomfield(json_encode(["bag_option" => $customFeeCheck]));
            $quote->save();
            return $result->setData(['success' => $quote->getData()]);
        } catch (\Exception $e) {
            return $result->setData(['error' => $e->getMessage()]);
        }
    }
}

This controller receives two attributes the quoteId and whether the field is checked or unchecked. i use the quote.

In my case i use the quote because there is the information of the purchase and I set the field if it is check or not. here there are two ways to set the information here, do not use the quote and use a checkoutSession or see what information you have in the quote that you can set and then use it in the Model/Total/Customfee.php. to know which data you have on the quote do this $quote->getData()(you can see if you can reuse something or not)

Now let's dig into the UI As I mentioned before, I've created a new step for my input check box

file /view/frontend/web/js/view/bag-step.js

define([
    'jquery',
    'ko',
    'uiComponent',
    'Magento_Checkout/js/model/step-navigator',
    'Magento_Checkout/js/model/quote',
    'Magento_Checkout/js/model/totals',
    'Magento_Checkout/js/model/cart/totals-processor/default',
    'Magento_Catalog/js/price-utils'
], function ($, ko, Component, stepNavigator, quote, totals, totalsDefaultProvider, priceUtils) {
    'use strict';

    var bagPrice = window.checkoutConfig.bagFeeSettings.fee_amount;
    var minimumFee = window.checkoutConfig.bagFeeSettings.minimum_order_amount;

    return Component.extend({
        defaults: {
            template: 'Vendor_Module/bag-step'
        },
        stepCode: 'bag',
        errorValidationMessage: ko.observable(false),
        isCustomFeeCheck: ko.observable(false),
        initialize: function () {
            this._super();
            var self = this;
            
            if (!parseInt(window.checkoutConfig.bagFeeSettings.show_hide_block)) {
                $(".checkout-bag-step.hoverable").hide();
            }
            
            var bss_customfield = JSON.parse(window.checkoutConfig.quoteData.bss_customfield)
            if (bss_customfield && bss_customfield.bag_option === 'true') {
                this.isCustomFeeCheck(true);
            }


            totals.totals.subscribe(function(data) {
                if (data.base_subtotal <= minimumFee) {
                    self.getFeeAmount(priceUtils.formatPrice(bagPrice, quote.getPriceFormat()));
                } else {
                    self.getFeeAmount(priceUtils.formatPrice(0, quote.getPriceFormat()));
                }
            });

            return this;
        },
        getFeeAmount: ko.observable(priceUtils.formatPrice(bagPrice, quote.getPriceFormat())),
        getMinimumFee: priceUtils.formatPrice(minimumFee, quote.getPriceFormat()),
        handleCheckbox: function () {
            $.ajax({
                url: `/${checkoutConfig.storeCode}/customFee/index/index`,
                method: 'POST',
                beforeSend: function() {
                    totals.isLoading(true);
                },
                data: {
                    quote_id: quote.getQuoteId(),
                    enable_custom_fee: this.isCustomFeeCheck(),
                },
                dataType: 'json',
                success: function () {
                    totalsDefaultProvider.estimateTotals(quote.shippingAddress());
                },
                error: function (xhr, status, error) {
                    console.log('Error: ' + error);
                }
            }).always(function() {
                totals.isLoading(false);
            });
            
            return true;
        }
    });
});

Take a look to the handleCheckbox method, there is the key:

  1. We call the controller we have just created.
  2. The controller set the data
  3. onSuccess call totalsDefaultProvider.estimateTotals(quote.shippingAddress());
  4. estimateTotals trigger the Model/Total/Customfee.php file
  5. Customfee.php file validates whether the box is checked or unchecked if (isset($data->bag_option) && $data->bag_option === "true") { base on that applies the fee or not
  6. Magento automatically updates prices in the checkout summary

Please note that I have a lot of bss dependencies but you can replace this with a session hope this helps.

enter image description here

Your Answer

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

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