Magento2 | PWA | GraphQL

Add Extra Fee to customer for each order based on fixed amount with or Without tax at Checkout Magento2


In this post, we will check how to add an extra Fee to customer for each order based on fixed amount with or without tax at Checkout Page Magento2.

Let's start it by creating custom module.

You can find complete module on Github at Magelearn_CustomFee







Create folder inside app/code/Magelearn/CustomFee

Add registration.php file in it:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Magelearn_CustomFee',
    __DIR__
);

Add composer.json file in it:

{
    "name": "magelearn/module-custom-fee",
    "description": "A Magento 2 module that adds custom extra fee to customer for each order based on fixed amount with or without tax at checkout",
    "type": "magento2-module",
    "license": "proprietary",
    "authors": [
        {
            "name": "vijay rami",
            "email": "vijaymrami@gmail.com"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "files": [ "registration.php" ],
        "psr-4": {
            "Magelearn\\CustomFee\\": ""
        }
    }
}

Add etc/module.xml file in it:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Magelearn_CustomFee" setup_version="1.0.0"></module>
</config>
Now, we will add database setup script.
Create db_schema.xml file in etc folder.
this script will add new column `fee`, `fee_tax` and `gift_fee` in `sales_order` , `quote` , `quote_address` , `sales_invoice` and `sales_creditmemo`  table.

<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="sales_order" resource="sales" engine="innodb" comment="Sales Order Fee and Base Fee Attribute">
        <column xsi:type="decimal" name="fee" nullable="true" default="0.00" comment="Fee"/>
        <column xsi:type="float" name="fee_tax" nullable="true" default="0.00" comment="Fee Tax"/>
        <column xsi:type="int" name="gift_fee_check" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Gift Fee Check"/>
    </table>
    <table name="quote" resource="checkout" engine="innodb" comment="Quote Fee and Base Fee Attribute">
        <column xsi:type="decimal" name="fee" nullable="true" default="0.00" comment="Fee"/>
        <column xsi:type="float" name="fee_tax" nullable="true" default="0.00" comment="Fee Tax"/>
        <column xsi:type="int" name="gift_fee_check" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Gift Fee Check"/>
    </table>
    <table name="quote_address" resource="checkout" engine="innodb" comment="Quote Address Fee and Base Fee Attribute">
        <column xsi:type="decimal" name="fee" nullable="true" default="0.00" comment="Fee"/>
        <column xsi:type="float" name="fee_tax" nullable="true" default="0.00" comment="Fee Tax"/>
        <column xsi:type="int" name="gift_fee_check" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Gift Fee Check"/>
    </table>
    <table name="sales_invoice" resource="checkout" engine="innodb" comment="Sales Invoice Fee and Base Fee Attribute">
        <column xsi:type="decimal" name="fee" nullable="true" default="0.00" comment="Fee"/>
        <column xsi:type="float" name="fee_tax" nullable="true" default="0.00" comment="Fee Tax"/>
    </table>
    <table name="sales_creditmemo" resource="checkout" engine="innodb" comment="Sales Credit memo Fee and Base Fee Attribute">
        <column xsi:type="decimal" name="fee" nullable="true" default="0.00" comment="Fee"/>
        <column xsi:type="float" name="fee_tax" nullable="true" default="0.00" comment="Fee Tax"/>
    </table>
</schema>

To validate this db_schema.xml file we will first run below command:

php bin/magento setup:db-declaration:generate-whitelist --module-name=Magelearn_CustomFee

This command will automatically generate db_schema_whitelist.json file inside etc folder.

First we will provide some default configurations for this new field.

Add app/code/Magelearn/CustomFee/etc/adminhtml/system.xml file.
<?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="customfee" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
            <class>separator-top</class>
            <label>Custom Fee</label>
            <tab>sales</tab>
            <resource>Magelearn_CustomFee::config_customfee</resource>
            <group id="customfee" translate="label" type="text" sortOrder="500" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Custom Fee Configuration</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\Enabledisable</source_model>
                </field>
                <field id="name" translate="label comment" type="text" sortOrder="2" showInDefault="1"
                       showInWebsite="1" showInStore="1">
                    <label>Fee Name</label>
                    <depends>
                        <field id="customfee/customfee/status">1</field>
                    </depends>         
                </field>
                <field id="customfee_amount" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Custom Fee Amount</label>
                    <validate>validate-number validate-zero-or-greater required</validate>
					<comment><![CDATA[Applies as Fixed Amount]]></comment>
					<depends>
                        <field id="customfee/customfee/status">1</field>
                    </depends>
                </field>
                <field id="minimum_order_amount" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Minimum Order Amount To Display Custom Fee</label>
                    <comment><![CDATA[Greaterthan or equal to]]></comment>
                    <validate>validate-number validate-zero-or-greater</validate>
                    <depends>
                        <field id="customfee/customfee/status">1</field>
                    </depends>
                </field>
            </group>
            <group id="tax" translate="label" type="text" sortOrder="600" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Custom Fee Tax</label>
                <field id="enable" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Calculate Tax On Custom Fee</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="tax_class" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Tax Class</label>
                    <source_model>Magento\Tax\Model\TaxClass\Source\Product</source_model>
                    <depends><field id="customfee/tax/enable">1</field></depends>
                </field>
                <field id="display" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Custom Fee</label>
                    <source_model>Magento\Tax\Model\System\Config\Source\Tax\Display\Type</source_model>
                    <depends><field id="customfee/tax/enable">1</field></depends>
                </field>
            </group>
        </section>
    </system>
</config>

And will set these options default values in etc/config.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <customfee>
        	<customfee>
                <status>1</status>
                <name>Custom Fee</name>
                <customfee_amount>10</customfee_amount>
                <minimum_order_amount>30</minimum_order_amount>
            </customfee>
        </customfee>
    </default>
</config>

Add etc/acl.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::stores">
                    <resource id="Magento_Backend::stores_settings">
                        <resource id="Magento_Config::config">
                            <resource id="Magelearn_CustomFee::config_customfee" title="Magelearn CustomFee" />
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>
Now we will add etc/di.xml file.
Here, we will add this additional attributes in Quote object with plugin type name="Magento\Checkout\Model\ShippingInformationManagement" 
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Model\ShippingInformationManagement">
        <plugin name="magelearn_save_custom_fee_in_quote" type="Magelearn\CustomFee\Plugin\Checkout\Model\ShippingInformationManagement" sortOrder="1"/>
    </type>
</config>

Now we will create our plugin file and add `fee` field inside beforeSaveAddressInformation function.
Create Plugin/Checkout/Model/ShippingInformationManagement.php file.
<?php
namespace Magelearn\CustomFee\Plugin\Checkout\Model;


class ShippingInformationManagement
{
    /**
     * @var \Magento\Quote\Model\QuoteRepository
     */
    protected $quoteRepository;

    /**
     * @var \Magelearn\CustomFee\Helper\Data
     */
    protected $dataHelper;

    /**
     * @param \Magento\Quote\Model\QuoteRepository $quoteRepository
     * @param \Magelearn\CustomFee\Helper\Data $dataHelper
     */
    public function __construct(
        \Magento\Quote\Model\QuoteRepository $quoteRepository,
        \Magelearn\CustomFee\Helper\Data $dataHelper
    )
    {
        $this->quoteRepository = $quoteRepository;
        $this->dataHelper = $dataHelper;
    }

    /**
     * @param \Magento\Checkout\Model\ShippingInformationManagement $subject
     * @param $cartId
     * @param \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation
     */
    public function beforeSaveAddressInformation(
        \Magento\Checkout\Model\ShippingInformationManagement $subject,
        $cartId,
        \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation
    )
    {
        $customFee = $addressInformation->getExtensionAttributes()->getFee();
		$Giftfeecheck = $addressInformation->getExtensionAttributes()->getGiftFeeCheck();
        $quote = $this->quoteRepository->getActive($cartId);
		
        if ($customFee) {
            $fee = $this->dataHelper->getCustomFee();
            $quote->setFee($fee);
        } else {
            $quote->setFee(NULL);
        }
		
		if ($Giftfeecheck) {
            $quote->setGiftFeeCheck($Giftfeecheck);
        } else {
            $quote->setGiftFeeCheck(0);
        }
    }
}

As per highlighted code above, we will add Helper/Data.php file.

<?php

namespace Magelearn\CustomFee\Helper;

use Magento\Framework\App\Helper\AbstractHelper;

class Data extends AbstractHelper
{
    /**
     * Custom fee config path
     */
    const CONFIG_CUSTOM_IS_ENABLED = 'customfee/customfee/status';
    const CONFIG_CUSTOM_FEE = 'customfee/customfee/customfee_amount';
    const CONFIG_FEE_LABEL = 'customfee/customfee/name';
    const CONFIG_MINIMUM_ORDER_AMOUNT = 'customfee/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 custom fee
     *
     * @return mixed
     */
    public function getCustomFee()
    {
        $storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
        $fee = $this->scopeConfig->getValue(self::CONFIG_CUSTOM_FEE, $storeScope);
        return $fee;
    }

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

    /**
     * @return mixed
     */
    public function getMinimumOrderAmount()
    {

        $storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
        $MinimumOrderAmount = $this->scopeConfig->getValue(self::CONFIG_MINIMUM_ORDER_AMOUNT, $storeScope);
        return $MinimumOrderAmount;
    }
}

Now we will add our events.xml file in which sales_model_service_quote_submit_before event will be used to save order object to quote object and payment_cart_collect_items_and_amounts event will be used to add custom fee amount in order.

Add etc/events.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_model_service_quote_submit_before">
        <observer name="paymentfee" instance="Magelearn\CustomFee\Observer\AddFeeToOrderObserver" />
    </event>
    <event name="payment_cart_collect_items_and_amounts">
        <observer name="magelearn_extrafee_payment_collect_total" instance="Magelearn\CustomFee\Observer\AddCustomfeetototal" />
    </event>
</config>

Now add Observer/AddFeeToOrderObserver.php file.

<?php
namespace Magelearn\CustomFee\Observer;

use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;

class AddFeeToOrderObserver implements ObserverInterface
{
	/**
     * @var \Magento\Framework\DataObject\Copy
     */
    protected $objectCopyService;
	
	/**
     * AddFeeToOrderObserver constructor.
     */
     /**
     * @param \Magento\Framework\DataObject\Copy $objectCopyService
     */
    public function __construct(
        \Magento\Framework\DataObject\Copy $objectCopyService
    ) {
		$this->objectCopyService = $objectCopyService;
    }
	
    /**
     * Set payment fee to order
     *
     * @param EventObserver $observer
     * @return $this
     */
    public function execute(EventObserver $observer)
    {
        /** @var \Magento\Sales\Model\Order $order */
        $order = $observer->getEvent()->getData('order');
        /** @var \Magento\Quote\Model\Quote $quote */
        $quote = $observer->getEvent()->getData('quote');
		
        $CustomFeeFee = $quote->getFee();
		
        if (!$CustomFeeFee) {
            return $this;
        }
        $order->setFee($CustomFeeFee);
		$this->objectCopyService->copyFieldsetToTarget('sales_convert_quote', 'to_order', $quote, $order);
        return $this;
    }
}

Also add Observer/AddCustomfeetototal.php file.

<?php

namespace Magelearn\CustomFee\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Store\Model\StoreManagerInterface;
use \Magento\Checkout\Model\Session;
use Magelearn\CustomFee\Helper\Data;

class AddCustomfeetototal implements ObserverInterface
{
    protected $checkout;
    protected $helper;

    public function __construct(
    	Session $checkout,
		Data $helper
    )
    {
        $this->checkout = $checkout;
        $this->helper = $helper;
    }

    public function execute(Observer $observer)
    {
        if(!$this->helper->isModuleEnabled()){
            return $this;
        }
        $cart = $observer->getEvent()->getCart();
        $quote = $this->checkout->getQuote();
        $customAmount = $quote->getFee();
        $label = $this->helper->getFeeLabel();
        if($customAmount) {
            $cart->addCustomItem($label, 1, $customAmount, $label);
        }
    }
}

Here, addCustomItem() function is added your custom amount field in cart and then payment method will also manages the amount as well.

In this, first parameter is the field name, i.e. amount field name. $label

Second parameter is quantity for the field.

Third parameter is amount, which you want to add. $customAmount

Forth parameter is identifier, i.e. id of your custom amount field. $label

Now add etc/extension_attributes.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Checkout\Api\Data\ShippingInformationInterface">
        <attribute code="fee" type="integer"/>
        <attribute code="gift_fee_check" type="integer"/>
    </extension_attributes>
</config>

Add Now add etc/fieldset.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:DataObject/etc/fieldset.xsd">
    <scope id="global">
        <fieldset id="sales_convert_quote_address">
            <field name="fee">
                <aspect name="to_order"/>
            </field>
            <field name="gift_fee_check">
                <aspect name="to_order"/>
            </field>
        </fieldset>
        <fieldset id="sales_convert_quote">
            <field name="fee">
                <aspect name="to_order"/>
            </field>
            <field name="gift_fee_check">
                <aspect name="to_order"/>
            </field>
        </fieldset>
    </scope>
</config>

Now add etc/frontend/di.xml file. Here we will add Configuration Provider array to be available at checkout page.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Checkout\Model\CompositeConfigProvider">
        <arguments>
            <argument name="configProviders" xsi:type="array">
                <item name="checkout_customfee_block" xsi:type="object">Magelearn\CustomFee\Model\CustomFeeConfigProvider</item>
            </argument>
        </arguments>
    </type>
</config>

Now add Model/CustomFeeConfigProvider.php file.

<?php
namespace Magelearn\CustomFee\Model;

use Magento\Checkout\Model\ConfigProviderInterface;
use Magento\Quote\Model\Quote;

class CustomFeeConfigProvider implements ConfigProviderInterface
{
    /**
     * @var \Magelearn\CustomFee\Helper\Data
     */
    protected $dataHelper;

    /**
     * @var \Magento\Checkout\Model\Session
     */
    protected $checkoutSession;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;
	
	protected $taxHelper;

    /**
     * @param \Magelearn\CustomFee\Helper\Data $dataHelper
     * @param \Magento\Checkout\Model\Session $checkoutSession
     * @param \Psr\Log\LoggerInterface $logger
	 * @param \Magelearn\CustomFee\Helper\Tax $helperTax
     */
    public function __construct(
        \Magelearn\CustomFee\Helper\Data $dataHelper,
        \Magento\Checkout\Model\Session $checkoutSession,
        \Psr\Log\LoggerInterface $logger,
        \Magelearn\CustomFee\Helper\Tax $helperTax
    )
    {
        $this->dataHelper = $dataHelper;
        $this->checkoutSession = $checkoutSession;
        $this->logger = $logger;
		$this->taxHelper = $helperTax;
    }

    /**
     * @return array
     */
    public function getConfig()
    {
        $customFeeConfig = [];
		
        $enabled = $this->dataHelper->isModuleEnabled();
        $minimumOrderAmount = $this->dataHelper->getMinimumOrderAmount();
		
        $customFeeConfig['fee_label'] = $this->dataHelper->getFeeLabel();
		
        $quote = $this->checkoutSession->getQuote();
        $subtotal = $quote->getSubtotal();
		
        $customFeeConfig['custom_fee_amount'] = $this->dataHelper->getCustomFee();
		
		if ($this->taxHelper->isTaxEnabled() && $this->taxHelper->displayInclTax()) {
            $address = $this->_getAddressFromQuote($quote);
            $customFeeConfig['custom_fee_amount'] = $this->dataHelper->getCustomFee() + $address->getFeeTax();
        }
        if ($this->taxHelper->isTaxEnabled() && $this->taxHelper->displayBothTax()) {

            $address = $this->_getAddressFromQuote($quote);
            $customFeeConfig['custom_fee_amount'] = $this->dataHelper->getCustomFee();
            $customFeeConfig['custom_fee_amount_inc'] = $this->dataHelper->getCustomFee() + $address->getFeeTax();
        }
		$customFeeConfig['displayInclTax'] = $this->taxHelper->displayInclTax();
        $customFeeConfig['displayExclTax'] = $this->taxHelper->displayExclTax();
        $customFeeConfig['displayBoth'] = $this->taxHelper->displayBothTax();
        $customFeeConfig['exclTaxPostfix'] = __('Excl. Tax');
        $customFeeConfig['inclTaxPostfix'] = __('Incl. Tax');
        $customFeeConfig['TaxEnabled'] = $this->taxHelper->isTaxEnabled();
		
		
        $customFeeConfig['show_hide_customfee_block'] = ($enabled && ($minimumOrderAmount <= $subtotal) && $quote->getFee()) ? true : false;
        $customFeeConfig['show_hide_customfee_shipblock'] = ($enabled && ($minimumOrderAmount <= $subtotal)) ? true : false;
        return $customFeeConfig;
    }

	protected function _getAddressFromQuote(Quote $quote)
    {
        return $quote->isVirtual() ? $quote->getBillingAddress() : $quote->getShippingAddress();
    }
}

Now we will add etc/sales.xml file. 

<?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="fee" instance="Magelearn\CustomFee\Model\Quote\Total\Fee" sort_order="150"/>
        </group>
    </section>
 	<section name="order_invoice">
        <group name="totals">
            <item name="fee" instance="Magelearn\CustomFee\Model\Invoice\Total\Fee" sort_order="160"/>
        </group>
    </section>
 	<section name="order_creditmemo">
        <group name="totals">
            <item name="fee" instance="Magelearn\CustomFee\Model\Creditmemo\Total\Fee" sort_order="160"/>
        </group>
    </section>
</config>

Here we have added a total item for "quote", "order_invoice" and "order_creditmemo" sections.

And the instance class to handle the calculation and collection of this total item must extend the
Magento\Quote\Model\Quote\Address\Total\AbstractTotal and implement the collect and fetch methods.

The collect method is used to calculate the value of our total,
while the fetch method returns the result along with the total collector’s code and its name.

Now first add Magelearn/CustomFee/Model/Total/Fee.php file.

<?php

namespace Magelearn\CustomFee\Model\Total;

use Magento\Store\Model\ScopeInterface;

class Fee extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal
{

    protected $helperData;

    /**
     * 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,
        \Magelearn\CustomFee\Helper\Data $helperData
	) {
        $this->quoteValidator = $quoteValidator;
        $this->helperData = $helperData;
    }

    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);
        if (!count($shippingAssignment->getItems())) {
            return $this;
        }

        $enabled = $this->helperData->isModuleEnabled();
        $minimumOrderAmount = $this->helperData->getMinimumOrderAmount();
        $subtotal = $total->getTotalAmount('subtotal');
        if ($enabled && $minimumOrderAmount <= $subtotal) {
            $fee = $quote->getFee();
            $total->setTotalAmount('fee', $fee);
            $total->setBaseTotalAmount('fee', $fee);
            $total->setFee($fee);
            $quote->setFee($fee);
            $total->setGrandTotal($total->getGrandTotal() + $fee);
            $total->setBaseGrandTotal($total->getBaseGrandTotal() + $fee);
        }
        return $this;
    }

    /**
     * @param \Magento\Quote\Model\Quote $quote
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     * @return array
     */
    public function fetch(
    	\Magento\Quote\Model\Quote $quote,
    	\Magento\Quote\Model\Quote\Address\Total $total
	) {

        $enabled = $this->helperData->isModuleEnabled();
        $minimumOrderAmount = $this->helperData->getMinimumOrderAmount();
        $subtotal = $quote->getSubtotal();
        $fee = $quote->getFee();
        if ($enabled && $minimumOrderAmount <= $subtotal && $fee) {
            return [
                'code' => 'fee',
                'title' => 'Custom Fee',
                'value' => $fee
            ];
        } else {
            return array();
        }
    }

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

    /**
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     */
    protected function clearValues(\Magento\Quote\Model\Quote\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);

    }
}

Now add Magelearn/CustomFee/Model/Quote/Total/Fee.php file.

<?php

namespace Magelearn\CustomFee\Model\Quote\Total;

use Magento\Store\Model\ScopeInterface;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Model\Quote\Address\Total;

class Fee extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal
{
	protected $quoteValidator = null;
	protected $_priceCurrency;
    protected $helperData;
	protected $taxHelper;
    private $taxCalculator;
	private $productMetadata;
    /**
     * 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
     */
    public function __construct(
    	\Magento\Quote\Model\QuoteValidator $quoteValidator,
    	\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
        \Magelearn\CustomFee\Helper\Data $helperData,
        \Magelearn\CustomFee\Helper\Tax $helperTax,
		\Magento\Tax\Model\Calculation $taxCalculator,
		\Magento\Framework\App\ProductMetadataInterface $productMetadata
	) {
        $this->quoteValidator = $quoteValidator;
		$this->_priceCurrency = $priceCurrency;
        $this->helperData = $helperData;
		$this->taxHelper = $helperTax;
        $this->taxCalculator = $taxCalculator;
		$this->productMetadata = $productMetadata;
    }

    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);
        if (!count($shippingAssignment->getItems())) {
            return $this;
        }

        $enabled = $this->helperData->isModuleEnabled();
        $minimumOrderAmount = $this->helperData->getMinimumOrderAmount();
        $subtotal = $total->getTotalAmount('subtotal');
        if ($enabled && $minimumOrderAmount <= $subtotal) {
            $fee = $this->helperData->getCustomFee();
            //Try to test with sample value
            //$fee=50;
            $total->setTotalAmount('fee', $fee);
            $total->setBaseTotalAmount('fee', $fee);
            $total->setFee($fee);
			$quote->setFee($fee);
			
            $version = (float)$this->getMagentoVersion();
			
			if($version > 2.1)
			{
				//$total->setGrandTotal($total->getGrandTotal() + $fee);
			}
			else
			{
				$total->setGrandTotal($total->getGrandTotal() + $fee);
			}
			
			if ($this->taxHelper->isTaxEnabled()) {
                $address = $this->_getAddressFromQuote($quote);
                $this->_calculateTax($address, $total);

                $extraTaxables = $address->getAssociatedTaxables();
                $extraTaxables[] = [
                    'code' => 'fee',
                    'type' => 'fee',
                    'quantity' => 1,
                    'tax_class_id' => $this->taxHelper->getTaxClassId(),
                    'unit_price' => $fee,
                    'base_unit_price' => $fee,
                    'price_includes_tax' => false,
                    'associated_item_code' => false
                ];

                $address->setAssociatedTaxables($extraTaxables);
            }
        }
        return $this;
    }

	public function getMagentoVersion()
	{
	    return $this->productMetadata->getVersion();
	}
	
    /**
     * @param \Magento\Quote\Model\Quote $quote
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     * @return array
     */
    public function fetch(
    	\Magento\Quote\Model\Quote $quote,
    	\Magento\Quote\Model\Quote\Address\Total $total
	) {

        $enabled = $this->helperData->isModuleEnabled();
        $minimumOrderAmount = $this->helperData->getMinimumOrderAmount();
        $subtotal = $quote->getSubtotal();
        $fee = $quote->getFee();
		$address = $this->_getAddressFromQuote($quote);

        $result = [];
        if ($enabled && ($minimumOrderAmount <= $subtotal) && $fee) {
            $result = [
                'code' => 'fee',
                'title' => $this->helperData->getFeeLabel(),
                'value' => $fee
            ];
			if ($this->taxHelper->isTaxEnabled() && $this->taxHelper->displayInclTax()) {
                 $result [] = [
                    'code' => 'fee',
                    'value' => $fee + $address->getFeeTax(),
                    'title' => __($this->helperData->getFeeLabel()),
                ];
            }
			if ($this->taxHelper->isTaxEnabled() && $this->taxHelper->displayBothTax()) {
                $result [] = [
                    'code' => 'fee',
                    'value' => $fee + $address->getFeeTax(),
                    'title' => __($this->helperData->getFeeLabel()),
                ];
            }
        }
        return $result;
    }

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

    /**
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     */
    protected function clearValues(\Magento\Quote\Model\Quote\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);

    }
	protected function _getAddressFromQuote(Quote $quote)
    {
        return $quote->isVirtual() ? $quote->getBillingAddress() : $quote->getShippingAddress();
    }
	protected function _calculateTax(Address $address, Total $total)
    {
        $taxClassId = $this->taxHelper->getTaxClassId();
        if (!$taxClassId) {
            return $this;
        }

        $taxRateRequest = $this->_getAddressTaxRequest($address);
        $taxRateRequest->setProductClassId($taxClassId);

        $rate = $this->taxCalculator->getRate($taxRateRequest);



        $baseTax = $this->taxCalculator->calcTaxAmount(
            $total->getBaseTotalAmount('fee'),
            $rate,
            false,
            true
        );
        $tax = $this->taxCalculator->calcTaxAmount(
            $total->getTotalAmount('fee'),
            $rate,
            false,
            true
        );



        //$total->setBaseMcPaymentfeeTaxAmount($baseTax);
        $total->setFeeTax($tax);

        $appliedRates = $this->taxCalculator->getAppliedRates($taxRateRequest);
        $this->_saveAppliedTaxes($address, $appliedRates, $tax, $baseTax, $rate);

        $total->addBaseTotalAmount('tax', $baseTax);
        $total->addTotalAmount('tax', $tax);

        return $this;
    }
	protected function _getAddressTaxRequest($address)
    {
        $addressTaxRequest = $this->taxCalculator->getRateRequest(
            $address,
            $address->getQuote()->getBillingAddress(),
            $address->getQuote()->getCustomerTaxClassId(),
            $address->getQuote()->getStore()
        );
        return $addressTaxRequest;
    }
	protected function _saveAppliedTaxes(
        Address $address,
        $applied,
        $amount,
        $baseAmount,
        $rate
    ) {
        $previouslyAppliedTaxes = $address->getAppliedTaxes();
        $process = 0;
        if(is_array($previouslyAppliedTaxes)) {
            $process = count($previouslyAppliedTaxes);
        }
        foreach ($applied as $row) {
            if ($row['percent'] == 0) {
                continue;
            }
            if (!isset($previouslyAppliedTaxes[$row['id']])) {
                $row['process'] = $process;
                $row['amount'] = 0;
                $row['base_amount'] = 0;
                $previouslyAppliedTaxes[$row['id']] = $row;
            }

            if ($row['percent'] !== null) {
                $row['percent'] = $row['percent'] ? $row['percent'] : 1;
                $rate = $rate ? $rate : 1;

                $appliedAmount = $amount / $rate * $row['percent'];
                $baseAppliedAmount = $baseAmount / $rate * $row['percent'];
            } else {
                $appliedAmount = 0;
                $baseAppliedAmount = 0;
                foreach ($row['rates'] as $rate) {
                    $appliedAmount += $rate['amount'];
                    $baseAppliedAmount += $rate['base_amount'];
                }
            }

            if ($appliedAmount || $previouslyAppliedTaxes[$row['id']]['amount']) {
                $previouslyAppliedTaxes[$row['id']]['amount'] += $appliedAmount;
                $previouslyAppliedTaxes[$row['id']]['base_amount'] += $baseAppliedAmount;
            } else {
                unset($previouslyAppliedTaxes[$row['id']]);
            }
        }
        $address->setAppliedTaxes($previouslyAppliedTaxes);
    }
}

Now as per highlighted code above, we will create Helper/Tax.php file.

<?php
namespace Magelearn\CustomFee\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
class Tax extends AbstractHelper
{
    const XML_PATH_TAX_ENABLED = 'customfee/tax/enable';
    const XML_PATH_TAX_CLASS   = 'customfee/tax/tax_class';
    const XML_PATH_TAX_DISPLAY = 'customfee/tax/display';

    public function isTaxEnabled()
    {
        return $this->scopeConfig->isSetFlag(self::XML_PATH_TAX_ENABLED, 'store');
    }

    public function getTaxClassId()
    {
        return $this->scopeConfig->getValue(self::XML_PATH_TAX_CLASS, 'store');
    }

    public function getTaxDisplay()
    {
        return $this->scopeConfig->getValue(self::XML_PATH_TAX_DISPLAY, 'store');
    }

    public function displayInclTax()
    {
        return in_array($this->getTaxDisplay(), [2]);
    }

    public function displayExclTax()
    {
        return in_array($this->getTaxDisplay(), [1]);
    }

    public function displayBothTax()
    {
        return in_array($this->getTaxDisplay(), [3]);
    }

    public function displaySuffix()
    {
        return ($this->getTaxDisplay() == 3);
    }
}

Now add Model/Invoice/Total/Fee.php file.

<?php

namespace Magelearn\CustomFee\Model\Invoice\Total;

use Magento\Sales\Model\Order\Invoice\Total\AbstractTotal;

class Fee extends AbstractTotal
{
    /**
     * @param \Magento\Sales\Model\Order\Invoice $invoice
     * @return $this
     */
    public function collect(\Magento\Sales\Model\Order\Invoice $invoice)
    {
        $invoice->setFee(0);

        $amount = $invoice->getOrder()->getFee();
        $invoice->setFee($amount);

        $invoice->setGrandTotal($invoice->getGrandTotal() + $invoice->getFee());
        $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() + $invoice->getFee());

        return $this;
    }
}

Now add Model/Creditmemo/Total/Fee.php file.

<?php

namespace Magelearn\CustomFee\Model\Creditmemo\Total;

use Magento\Sales\Model\Order\Creditmemo\Total\AbstractTotal;

class Fee extends AbstractTotal
{
    /**
     * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo
     * @return $this
     */
    public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo)
    {
        $creditmemo->setFee(0);

        $amount = $creditmemo->getOrder()->getFee();
        $creditmemo->setFee($amount);

        $creditmemo->setGrandTotal($creditmemo->getGrandTotal() + $creditmemo->getFee());
        $creditmemo->setBaseGrandTotal($creditmemo->getBaseGrandTotal() + $creditmemo->getFee());

        return $this;
    }
}

Now we will move on front end part.

First we will modify checkout_cart_index.xml file and display custom fee on checkout/cart page.

Add view/frontend/layout/checkout_cart_index.xml file.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.cart.totals">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="block-totals" xsi:type="array">
                            <item name="children" xsi:type="array">

                                <item name="fee" xsi:type="array">
                                    <item name="component"  xsi:type="string">Magelearn_CustomFee/js/view/checkout/cart/totals/fee</item>
                                    <item name="sortOrder" xsi:type="string">20</item>
                                    <item name="config" xsi:type="array">
                                         <item name="template" xsi:type="string">Magelearn_CustomFee/checkout/cart/totals/fee</item>
                                        <item name="title" xsi:type="string" translate="true">Custom Fee</item>
                                    </item>
                                </item>

                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

Now, as per the highlighted code above, we will add our JS component file and template file.

Add view/frontend/web/js/view/checkout/cart/totals/fee.js

define([
    'ko',
    'uiComponent',
    'Magento_Checkout/js/model/quote',
    'Magento_Catalog/js/price-utils',
    'Magento_Checkout/js/model/totals'

], function (ko, Component, quote, priceUtils, totals) {
    'use strict';
    var show_hide_customfee_blockConfig = window.checkoutConfig.show_hide_customfee_block;
    var fee_label = window.checkoutConfig.fee_label;
    var custom_fee_amount = window.checkoutConfig.custom_fee_amount;
    var custom_in_fee_amount = window.checkoutConfig.custom_fee_amount_inc;

    return Component.extend({
        totals: quote.getTotals(),
        canVisibleCustomFeeBlock: show_hide_customfee_blockConfig,
        getFormattedPrice: ko.observable(priceUtils.formatPrice(custom_fee_amount, quote.getPriceFormat())),
        getFeeLabel:ko.observable(fee_label),
        getInFeeLabel:ko.observable(window.checkoutConfig.inclTaxPostfix),
        getExFeeLabel:ko.observable(window.checkoutConfig.exclTaxPostfix),
        
        isDisplayed: function () {
            return this.getValue() != 0;
        },
        isDisplayBoth: function () {
            return window.checkoutConfig.displayBoth;
        },
        displayExclTax: function () {
            return window.checkoutConfig.displayExclTax;
        },
        displayInclTax: function () {
            return window.checkoutConfig.displayInclTax;
        },
        isTaxEnabled: function () {
            return window.checkoutConfig.TaxEnabled;
        },
        getValue: function() {
            var price = 0;
            if (this.totals() && totals.getSegment('fee')) {
                price = totals.getSegment('fee').value;
            }
            return price;
        },
        getInFormattedPrice: function() {
            var price = 0;
            if (this.totals() && totals.getSegment('fee')) {
                price = totals.getSegment('fee').value;
            }

            return priceUtils.formatPrice(price, quote.getPriceFormat());
        },
    });
});

And add view/frontend/web/template/checkout/cart/totals/fee.html

<!-- ko if: isDisplayed() -->
<!-- ko if: isTaxEnabled() -->
<!-- ko if: isDisplayBoth() -->
<tr class="totals fee excl" >
    <th class="mark" colspan="1" scope="row" data-bind="text: getFeeLabel() + ' ' + getExFeeLabel()"></th>
    <td class="amount">
        <span class="price" data-bind="text: getFormattedPrice()"></span>
    </td>
</tr>
<tr class="totals fee incl">
    <th class="mark" colspan="1" scope="row" data-bind="text: getFeeLabel() + ' ' + getInFeeLabel()"></th>
    <td class="amount">
        <span class="price" data-bind="text: getInFormattedPrice()"></span>
    </td>
</tr>
<!-- /ko -->
<!-- ko ifnot: isDisplayBoth() -->
<!-- ko if: displayExclTax() -->
<tr class="totals fee excl" >
    <th class="mark" colspan="1" scope="row" data-bind="text: getFeeLabel()"></th>
    <td class="amount">
        <span class="price" data-bind="text: getFormattedPrice()"></span>
    </td>
</tr>
<!-- /ko -->
<!-- ko if: displayInclTax() -->
<tr class="totals fee incl">
    <th class="mark" colspan="1" scope="row" data-bind="text: getFeeLabel()"></th>
    <td class="amount">
        <span class="price" data-bind="text: getInFormattedPrice()"></span>
    </td>
</tr>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- ko ifnot: isTaxEnabled() -->
<tr class="totals fee excl" >
    <th class="mark" colspan="1" scope="row" data-bind="text: getFeeLabel()"></th>
    <td class="amount">
        <span class="price" data-bind="text: getFormattedPrice()"></span>
    </td>
</tr>
<!-- /ko -->
<!-- /ko -->

Now, To display totals data properly on checkout summary, we will also create same files.

Add Magelearn/CustomFee/view/frontend/web/js/view/checkout/summary/fee.js

define(
    [
        'Magento_Checkout/js/view/summary/abstract-total',
        'Magento_Checkout/js/model/quote',
        'Magento_Catalog/js/price-utils',
        'Magento_Checkout/js/model/totals'
    ],
    function (Component, quote, priceUtils, totals) {
        "use strict";
        return Component.extend({
            defaults: {
                isFullTaxSummaryDisplayed: window.checkoutConfig.isFullTaxSummaryDisplayed || false,
                template: 'Magelearn_CustomFee/checkout/summary/fee'
            },
            totals: quote.getTotals(),
            isTaxDisplayedInGrandTotal: window.checkoutConfig.includeTaxInGrandTotal || false,
            isDisplayed: function() {
                return this.getValue() != 0;
            },
            getValue: function() {
                var price = 0;
                if (this.totals()) {
                    price = totals.getSegment('fee').value;
                }
                return this.getFormattedPrice(price);
            }
        });
    }
);

Add Magelearn/CustomFee/view/frontend/web/template/checkout/summary/fee.html

<!-- ko -->
  <tr class="totals fee excl">
    <th class="mark" scope="row">
        <span class="label" data-bind="text: title"></span>
        <span class="value" data-bind="text: getValue()"></span>
    </th>
    <td class="amount">
        <span class="price"
              data-bind="text: getValue(), attr: {'data-th': title}"></span>
    </td>
</tr>
<!-- /ko -->

Now, we will modify Magelearn/CustomFee/view/frontend/layout/checkout_index_index.xml file.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.root">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="checkout" xsi:type="array">
                            <item name="children" xsi:type="array">
                            	<item name="steps" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="shipping-step" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="shippingAddress" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="shippingAdditional" xsi:type="array">
                                                            <item name="component" xsi:type="string">uiComponent</item>
                                                            <item name="displayArea" xsi:type="string">shippingAdditional</item>
                                                            <item name="children" xsi:type="array">
                                                                <item name="gift_fee" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Magelearn_CustomFee/js/view/checkout/shipping/custom-fee</item>
                                                                </item>
                                                                <item name="gift_fee_error_text" xsi:type="array">
						                                            <item name="sortOrder" xsi:type="string">35</item>
						                                            <item name="component"  xsi:type="string">uiComponent</item>
						                                            <item name="config" xsi:type="array">
						                                                <item name="template" xsi:type="string">Magelearn_CustomFee/error_text</item>
						                                            </item>
						                                        </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                                
                                <item name="sidebar" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="summary" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="totals" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                       <item name="fee" xsi:type="array">
                                                            <item name="component"  xsi:type="string">Magelearn_CustomFee/js/view/checkout/cart/totals/fee</item>
                                                            <item name="sortOrder" xsi:type="string">20</item>
                                                            <item name="config" xsi:type="array">
                                                                 <item name="template" xsi:type="string">Magelearn_CustomFee/checkout/cart/totals/fee</item>
                                                               <!-- <item name="title" xsi:type="string" translate="true">Custom Fee</item>-->
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                                <item name="cart_items" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="details" xsi:type="array">
                                                            <item name="children" xsi:type="array">
                                                                <item name="subtotal" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Magento_Tax/js/view/checkout/summary/item/details/subtotal</item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>

                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

Now, as per the highlighted code above, we will add our JS Component file.
Add view/frontend/web/js/view/checkout/shipping/custom-fee.js file.

define([
    'jquery',
    'ko',
    'uiComponent',
    'Magento_Checkout/js/model/quote',
    'Magento_Catalog/js/price-utils',
    'Magento_Ui/js/modal/modal'

], function ($, ko, Component, quote, priceUtils,modal) {
    'use strict';
    var fee_label = window.checkoutConfig.fee_label;         
    var custom_fee_amount = window.checkoutConfig.custom_fee_amount;
    
    return Component.extend({
        defaults: {
            template: 'Magelearn_CustomFee/checkout/shipping/custom-fee',
            allowGiftFee: ko.observable(true),
            canVisibleCustomfeeBlock: window.checkoutConfig.show_hide_customfee_shipblock,
            getFormattedPrice: ko.observable(priceUtils.formatPrice(custom_fee_amount, quote.getPriceFormat())),
            getFeeLabel:ko.observable(fee_label)
            
        },
        initialize: function () {
            this._super();
            var self = this;
            $(document).on('change', 'input[name="gift_fee"]', function () {
                if($(this).prop('checked') == true) {
                    $(".gift-fee-error").hide();
                }
            });
        }
    });
});

Now as per highlighted above code, we will add template file at view/frontend/web/template/checkout/shipping/custom-fee.html

<div data-bind="visible: canVisibleCustomfeeBlock" data-trigger="trigger">
    <input type="checkbox" name="gift_fee_check" data-bind="checked: allowGiftFee" />
    <span data-bind="text: getFormattedPrice()" class="price" style="font-weight: 600"></span>
    <span data-bind="text: getFeeLabel()"></span>
</div>
<div data-bind="mageInit: {
        'Magento_Ui/js/modal/modal':{
            'type': 'popup',
            'title': 'Custom Fee',
            'trigger': '[data-trigger=trigger]',
            'responsive': true,
            'buttons': [{
                text: 'OK',
                class: 'action'
            }]
        }}">
    <div class="content">We are taking custom gift wrapping fee for this order. Please accept Custom Fee Checkbox before proceed.</div>
</div>

We will modify requirejs-config.js file and add some validation when going from next step from Shipping.

We will also add extension_attributes in payloadExtender to save its value further in database.

Add Magelearn/CustomFee/view/frontend/requirejs-config.js file.

var config = {
	map: {
       "*": {
           'Magento_Checkout/js/model/shipping-save-processor/default' : 'Magelearn_CustomFee/js/model/shipping-save-processor/default'
       },
  	},
    config: {
        mixins: {
            'Magento_Checkout/js/model/shipping-save-processor/payload-extender': {
            'Magelearn_CustomFee/js/model/shipping-save-processor/payload-extender-mixin': true
            }
        }
    }
};

Now add Magelearn/CustomFee/view/frontend/web/js/model/shipping-save-processor/default.js file.

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

define([
    'ko',
    'Magento_Checkout/js/model/quote',
    'Magento_Checkout/js/model/resource-url-manager',
    'mage/storage',
    'Magento_Checkout/js/model/payment-service',
    'Magento_Checkout/js/model/payment/method-converter',
    'Magento_Checkout/js/model/error-processor',
    'Magento_Checkout/js/model/full-screen-loader',
    'Magento_Checkout/js/action/select-billing-address',
    'Magento_Checkout/js/model/shipping-save-processor/payload-extender',
    'jquery',
    'Magento_Ui/js/modal/alert'
], function (
    ko,
    quote,
    resourceUrlManager,
    storage,
    paymentService,
    methodConverter,
    errorProcessor,
    fullScreenLoader,
    selectBillingAddressAction,
    payloadExtender,
    $,
    alert
) {
    'use strict';

    return {
        /**
         * @return {jQuery.Deferred}
         */
        saveShippingInformation: function () {
            var payload;
			
			var gift_fee = $('[name="gift_fee_check"]').prop("checked");
            if(gift_fee != true){
              alert({
                title: $.mage.__('Error'),
                content: $.mage.__('Please accept custom gift price for this order.')
              });
              $(".gift-fee-error").show(); // show hidden message
              $('[name="gift_fee_check"]').focus();
              return false;
            } else {
            	$(".gift-fee-error").hide();
            }
           
            if (!quote.billingAddress() && quote.shippingAddress().canUseForBilling()) {
                selectBillingAddressAction(quote.shippingAddress());
            }

            payload = {
                addressInformation: {
                    'shipping_address': quote.shippingAddress(),
                    'billing_address': quote.billingAddress(),
                    'shipping_method_code': quote.shippingMethod()['method_code'],
                    'shipping_carrier_code': quote.shippingMethod()['carrier_code']
                }
            };

            payloadExtender(payload);

            fullScreenLoader.startLoader();

            return storage.post(
                resourceUrlManager.getUrlForSetShippingInformation(quote),
                JSON.stringify(payload)
            ).done(
                function (response) {
                    quote.setTotals(response.totals);
                    paymentService.setPaymentMethods(methodConverter(response['payment_methods']));
                    fullScreenLoader.stopLoader();
                }
            ).fail(
                function (response) {
                    errorProcessor.process(response);
                    fullScreenLoader.stopLoader();
                }
            );
        }
    };
});

Add Magelearn/CustomFee/view/frontend/web/js/model/shipping-save-processor/payload-extender-mixin.js file.

define([
	'jquery',
	'underscore',
	'mage/utils/wrapper'
	], function ($, _, wrapper) {
    'use strict';
    return function (payloadExtender) {
        return wrapper.wrap(payloadExtender, function (originalPayloadExtender, payload) {
        	var gift_fee_val = $('[name="gift_fee_check"]').prop("checked") == true ? 1 : 0;
        	payload = originalPayloadExtender(payload);
        	          
            _.extend(payload.addressInformation,{
            	extension_attributes: {
                    'gift_fee_check': gift_fee_val
                }	
            });
            return payload
        });
    };
});

Now as per defined in checkout_index_index.xml file, we will also add our error_text html file.

Add Magelearn/CustomFee/view/frontend/web/template/error_text.html file.

<div class="gift-fee-error message error" role="alert" style="display:none;">
	<span class="gift-fee-error-message">Please accept gift fee custom amount for this order to further proceed.</span>
</div>

Now to display this custom fee at different places on frontend, we will modify different layout xml files.

Add Magelearn/CustomFee/view/frontend/layout/sales_order_view.xml file.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="order_totals">
            <block class="Magelearn\CustomFee\Block\Sales\Totals\Fee" name="fee"/>
        </referenceBlock>
    </body>
</page>

As per highlighted code above, we will add Magelearn/CustomFee/Block/Sales/Totals/Fee.php file.

<?php

namespace Magelearn\CustomFee\Block\Sales\Totals;

class Fee extends \Magento\Framework\View\Element\Template
{
    /**
     * @var \Magelearn\CustomFee\Helper\Data
     */
    protected $_dataHelper;

    /**
     * @var Order
     */
    protected $_order;

    /**
     * @var \Magento\Framework\DataObject
     */
    protected $_source;

    /**
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @param array $data
     */
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
         \Magelearn\CustomFee\Helper\Data $dataHelper,
        array $data = []
    )
    {
        $this->_dataHelper = $dataHelper;
        parent::__construct($context, $data);
    }

    /**
     * Check if we nedd display full tax total info
     *
     * @return bool
     */
    public function displayFullSummary()
    {
        return true;
    }

    /**
     * Get data (totals) source model
     *
     * @return \Magento\Framework\DataObject
     */
    public function getSource()
    {
        return $this->_source;
    }

    public function getStore()
    {
        return $this->_order->getStore();
    }

    /**
     * @return Order
     */
    public function getOrder()
    {
        return $this->_order;
    }

    /**
     * @return array
     */
    public function getLabelProperties()
    {
        return $this->getParentBlock()->getLabelProperties();
    }

    /**
     * @return array
     */
    public function getValueProperties()
    {
        return $this->getParentBlock()->getValueProperties();
    }

    /**
     * @return $this
     */
    public function initTotals()
    {
        $parent = $this->getParentBlock();
        $this->_order = $parent->getOrder();
        $this->_source = $parent->getSource();
       // $store = $this->getStore();

        $fee = new \Magento\Framework\DataObject(
            [
                'code' => 'fee',
                'strong' => false,
                'value' => $this->_source->getFee(),
                'label' => $this->_dataHelper->getFeeLabel(),
            ]
        );

        $parent->addTotal($fee, 'fee');

        return $this;
    }

}

Now we will define same XML nodes in different xml files.

We will add xml files to display custom totals inside Magelearn/CustomFee/view/frontend/layout

sales_order_invoice.xml 

sales_order_creditmemo.xml 

sales_email_order_creditmemo_items.xml

sales_email_order_invoice_items.xml

sales_email_order_items.xml

sales_order_print.xml

sales_guest_print.xml

sales_guest_view.xml

sales_guest_invoice.xml

sales_guest_printinvoice.xml

Now to display this custom fee at different places on adminhtml, we will modify different layout xml files.

Add Magelearn/CustomFee/view/adminhtml/layout/sales_order_view.xml file.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="order_totals">
            <block class="Magelearn\CustomFee\Block\Adminhtml\Sales\Totals" name="fee"/>
        </referenceContainer>
    </body>
</page>

As per highlighted code above, we will add Magelearn/CustomFee/Block/Adminhtml/Sales/Totals.php file.

<?php

namespace Magelearn\CustomFee\Block\Adminhtml\Sales;

class Totals extends \Magento\Framework\View\Element\Template
{

    /**
     * @var \Magelearn\CustomFee\Helper\Data
     */
    protected $_dataHelper;
   

    /**
     * @var \Magento\Directory\Model\Currency
     */
    protected $_currency;

    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        \Magelearn\CustomFee\Helper\Data $dataHelper,
        \Magento\Directory\Model\Currency $currency,
        array $data = []
    ) {
        parent::__construct($context, $data);
        $this->_dataHelper = $dataHelper;
        $this->_currency = $currency;
    }

    /**
     * Retrieve current order model instance
     *
     * @return \Magento\Sales\Model\Order
     */
    public function getOrder()
    {
        return $this->getParentBlock()->getOrder();
    }

    /**
     * @return mixed
     */
    public function getSource()
    {
        return $this->getParentBlock()->getSource();
    }

    /**
     * @return string
     */
    public function getCurrencySymbol()
    {
        return $this->_currency->getCurrencySymbol();
    }

    /**
     *
     *
     * @return $this
     */
    public function initTotals()
    {
        $this->getParentBlock();
        $this->getOrder();
        $this->getSource();

        if(!$this->getSource()->getFee()) {
            return $this;
        }
        $total = new \Magento\Framework\DataObject(
            [
                'code' => 'fee',
                'value' => $this->getSource()->getFee(),
                'label' => $this->_dataHelper->getFeeLabel(),
            ]
        );
        $this->getParentBlock()->addTotalBefore($total, 'grand_total');

        return $this;
    }
}

Add Magelearn/CustomFee/view/adminhtml/layout/sales_order_invoice_view.xml file.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="invoice_totals">
            <block class="Magelearn\CustomFee\Block\Adminhtml\Sales\Order\Invoice\Totals"
                   name="fee" as="fee">
            </block>
        </referenceBlock>
    </body>
</page>

As per highlighted code above, we will add Magelearn/CustomFee/Block/Adminhtml/Sales/Order/Invoice/Totals.php file.

<?php

namespace Magelearn\CustomFee\Block\Adminhtml\Sales\Order\Invoice;

class Totals extends \Magento\Framework\View\Element\Template
{

    /**
     * @var \Magelearn\CustomFee\Helper\Data
     */
    protected $_dataHelper;

    /**
     * Order invoice
     *
     * @var \Magento\Sales\Model\Order\Invoice|null
     */
    protected $_invoice = null;

    /**
     * @var \Magento\Framework\DataObject
     */
    protected $_source;

    /**
     * OrderFee constructor.
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @param array $data
     */
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        \Magelearn\CustomFee\Helper\Data $dataHelper,
        array $data = []
    ) {
        $this->_dataHelper = $dataHelper;
        parent::__construct($context, $data);
    }

    /**
     * Get data (totals) source model
     *
     * @return \Magento\Framework\DataObject
     */
    public function getSource()
    {
        return $this->getParentBlock()->getSource();
    }

    public function getInvoice()
    {
        return $this->getParentBlock()->getInvoice();
    }
    /**
     * Initialize payment fee totals
     *
     * @return $this
     */
    public function initTotals()
    {
        $this->getParentBlock();
        $this->getInvoice();
        $this->getSource();

        if(!$this->getSource()->getFee()) {
            return $this;
        }
        $total = new \Magento\Framework\DataObject(
            [
                'code' => 'fee',
                'value' => $this->getSource()->getFee(),
                'label' => $this->_dataHelper->getFeeLabel(),
            ]
        );

        $this->getParentBlock()->addTotalBefore($total, 'grand_total');
        return $this;
    }
}

Add Magelearn/CustomFee/view/adminhtml/layout/sales_order_creditmemo_view.xml file.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="creditmemo_totals">
            <block class="Magelearn\CustomFee\Block\Adminhtml\Sales\Order\Creditmemo\Totals"
                   name="fee" as="fee"/>
        </referenceBlock>
    </body>
</page>

As per highlighted code above, we will add Magelearn/CustomFee/Block/Adminhtml/Sales/Order/Creditmemo/Totals.php file.

<?php

namespace Magelearn\CustomFee\Block\Adminhtml\Sales\Order\Creditmemo;

class Totals extends \Magento\Framework\View\Element\Template
{
    /**
     * Order invoice
     *
     * @var \Magento\Sales\Model\Order\Creditmemo|null
     */
    protected $_creditmemo = null;

    /**
     * @var \Magento\Framework\DataObject
     */
    protected $_source;

    /**
     * @var \Magelearn\CustomFee\Helper\Data
     */
    protected $_dataHelper;

    /**
     * OrderFee constructor.
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @param array $data
     */
    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
         \Magelearn\CustomFee\Helper\Data $dataHelper,
        array $data = []
    ) {
        $this->_dataHelper = $dataHelper;
        parent::__construct($context, $data);
    }

    /**
     * Get data (totals) source model
     *
     * @return \Magento\Framework\DataObject
     */
    public function getSource()
    {
        return $this->getParentBlock()->getSource();
    }

    public function getCreditmemo()
    {
        return $this->getParentBlock()->getCreditmemo();
    }
    /**
     * Initialize payment fee totals
     *
     * @return $this
     */
    public function initTotals()
    {
        $this->getParentBlock();
        $this->getCreditmemo();
        $this->getSource();

        if(!$this->getSource()->getFee()) {
            return $this;
        }
        $fee = new \Magento\Framework\DataObject(
            [
                'code' => 'fee',
                'strong' => false,
                'value' => $this->getSource()->getFee(),
                'label' => $this->_dataHelper->getFeeLabel(),
            ]
        );

        $this->getParentBlock()->addTotalBefore($fee, 'grand_total');

        return $this;
    }
}

We will also add different layout file for admin at Magelearn/CustomFee/view/adminhtml/layout

sales_order_creditmemo_new.xml

sales_order_invoice_new.xml

sales_order_creditmemo_updateqty.xml

sales_order_invoice_updateqty.xml

0 Comments On "Add Extra Fee to customer for each order based on fixed amount with or Without tax at Checkout Magento2"

Back To Top