Gift wrapping, cash-on-delivery charges, rush delivery, payment processing fees — sooner or later most Magento 2 stores need to add an extra charge to the order total. Out of the box, Magento gives you no admin setting for this: order totals are computed by a chain of "total collector" classes, and adding your own fee means writing one.
This guide walks through the complete implementation — a custom module that adds a fee to the quote totals and displays it on the cart page, the checkout summary, and the customer's order view — with working code for every file and screenshots of the result.
11 Steps to Add an Extra Fee to Order Totals Programmatically in Magento 2
We'll build the module as Mageants_Fee — replace Mageants (Vendor_Name) and Fee (Module_Name) with your own throughout.
Step 1: Register the Module
Create app/code/Mageants/Fee/registration.php:
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Mageants_Fee',
__DIR__
);
|
Step 2: Configure module.xml
Create app/code/Mageants/Fee/etc/module.xml. The sequence ensures our module loads after the modules whose totals we're extending:
<?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="Mageants_Fee" setup_version="1.0.0">
<sequence>
<module name="Magento_Sales"/>
<module name="Magento_Quote"/>
<module name="Magento_Checkout"/>
</sequence>
</module>
</config>
|
Step 3: Define sales.xml for Total Calculations
In Magento, sales.xml declares every total object — subtotal, discount, tax, grand total — that appears on the cart page and in order details. This is where we register our fee collector and tell Magento where it sits in the calculation order via sort_order.
Create app/code/Mageants/Fee/etc/sales.xml:
<?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="Mageants\Fee\Model\Total\Fee" sort_order="150"/>
</group>
</section>
</config>
|
Step 4: Create the Fee.php Total Collector Model
This is the heart of the implementation. The collect() method adds the fee amount to the quote totals, and fetch() supplies the code, title, and value that the frontend components read. The fee is hardcoded to 10 here for clarity — in a real store you'd pull it from configuration or calculate it from the quote.
Create app/code/Mageants/Fee/Model/Total/Fee.php:
<?php
namespace Mageants\Fee\Model\Total;
class Fee extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal
{
/**
* Fee amount to charge (replace with a config value in production)
*/
const FEE_AMOUNT = 10;
/**
* Collect the fee into the address totals
*
* @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 collect(
\Magento\Quote\Model\Quote $quote,
\Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment,
\Magento\Quote\Model\Quote\Address\Total $total
) {
parent::collect($quote, $shippingAssignment, $total);
$existAmount = 0;
$fee = self::FEE_AMOUNT;
$balance = $fee - $existAmount;
$total->setTotalAmount('fee', $balance);
$total->setBaseTotalAmount('fee', $balance);
$total->setFee($balance);
$total->setBaseFee($balance);
$total->setGrandTotal($total->getGrandTotal());
$total->setBaseGrandTotal($total->getBaseGrandTotal());
return $this;
}
/**
* Assign the fee amount and label for display
*
* @param \Magento\Quote\Model\Quote $quote
* @param \Magento\Quote\Model\Quote\Address\Total $total
* @return array
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function fetch(
\Magento\Quote\Model\Quote $quote,
\Magento\Quote\Model\Quote\Address\Total $total
) {
return [
'code' => 'fee',
'title' => __('Custom Fee'),
'value' => self::FEE_AMOUNT
];
}
/**
* Get the total's label
*
* @return \Magento\Framework\Phrase
*/
public function getLabel()
{
return __('Custom Fee');
}
}
|
Step 5: Add the Frontend Templates
Two small Knockout templates render the fee row — one for the checkout summary, one for the cart totals.
fee.html for the Checkout Summary
Create app/code/Mageants/Fee/view/frontend/web/template/checkout/summary/fee.html:
<!-- ko -->
<tr class="totals fee">
<th class="mark" scope="row">
<span class="label" data-bind="text: title"></span>
</th>
<td class="amount">
<span class="price" data-bind="text: getValue(), attr: {'data-th': title}"></span>
</td>
</tr>
<!-- /ko -->
|
fee.html for the Cart Totals
Create app/code/Mageants/Fee/view/frontend/web/template/checkout/cart/totals/fee.html:
<!-- ko -->
<tr class="totals fee">
<th class="mark" colspan="1" scope="row" data-bind="text: title"></th>
<td class="amount">
<span class="price" data-bind="text: getValue()"></span>
</td>
</tr>
<!-- /ko -->
|
Step 6: Implement the JavaScript Components
These UI components read the fee segment from the totals data and feed it to the templates above, so the fee updates live as the cart changes.
fee.js for the Checkout Summary
Create app/code/Mageants/Fee/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: 'Mageants_Fee/checkout/summary/fee'
},
totals: quote.getTotals(),
isTaxDisplayedInGrandTotal: window.checkoutConfig.includeTaxInGrandTotal || false,
isDisplayed: function () {
return this.isFullMode();
},
getValue: function () {
var price = 0;
if (this.totals() && totals.getSegment('fee')) {
price = totals.getSegment('fee').value;
}
return this.getFormattedPrice(price);
},
getBaseValue: function () {
var price = 0;
if (this.totals()) {
price = this.totals().base_fee;
}
return priceUtils.formatPrice(price, quote.getBasePriceFormat());
}
});
}
);
|
fee.js for the Cart Totals
The cart version simply extends the summary component and forces display. Create app/code/Mageants/Fee/view/frontend/web/js/view/checkout/cart/totals/fee.js:
define(
[
'Mageants_Fee/js/view/checkout/summary/fee'
],
function (Component) {
'use strict';
return Component.extend({
/**
* @override
*/
isDisplayed: function () {
return true;
}
});
}
);
|
Step 7: Add the Fee to the Cart Page Layout
Create app/code/Mageants/Fee/view/frontend/layout/checkout_cart_index.xml to inject the fee row into the cart totals block:
<?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">Mageants_Fee/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">Mageants_Fee/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>
|
Step 8: Add the Fee to the Checkout Page Layout
Create app/code/Mageants/Fee/view/frontend/layout/checkout_index_index.xml. Note that the checkout page uses the summary component and template — not the cart ones — so the fee only shows once the summary is in full mode:
<?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="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">Mageants_Fee/js/view/checkout/summary/fee</item>
<item name="sortOrder" xsi:type="string">20</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Mageants_Fee/checkout/summary/fee</item>
<item name="title" xsi:type="string" translate="true">Custom Fee</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
|
Step 9: Display the Fee on the Customer Order View
So the fee also appears in the customer's "My Orders" section, create app/code/Mageants/Fee/view/frontend/layout/sales_order_view.xml:
<?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="Mageants\Fee\Block\Sales\Order\Fee" name="fee"/>
</referenceContainer>
</body>
</page>
|
Step 10: Create the Order Totals Block
This block registers the fee with the order totals renderer, so it appears in order views on both the frontend and the admin. Create app/code/Mageants/Fee/Block/Sales/Order/Fee.php:
<?php
namespace Mageants\Fee\Block\Sales\Order;
class Fee extends \Magento\Framework\View\Element\Template
{
/**
* @var \Magento\Sales\Model\Order
*/
protected $_order;
/**
* @var \Magento\Framework\DataObject
*/
protected $_source;
/**
* Get data (totals) source model
*
* @return \Magento\Framework\DataObject
*/
public function getSource()
{
return $this->_source;
}
/**
* @return \Magento\Sales\Model\Order
*/
public function getOrder()
{
return $this->_order;
}
/**
* Register the fee total with the parent totals block
*
* @return $this
*/
public function initTotals()
{
$parent = $this->getParentBlock();
$this->_order = $parent->getOrder();
$this->_source = $parent->getSource();
$fee = new \Magento\Framework\DataObject(
[
'code' => 'fee',
'strong' => false,
'value' => \Mageants\Fee\Model\Total\Fee::FEE_AMOUNT,
'label' => __('Custom Fee'),
]
);
$parent->addTotal($fee, 'fee');
return $this;
}
}
|
Step 11: Enable the Module and Deploy
Run these commands from the Magento root, then clear your browser cache before testing:
php bin/magento module:enable Mageants_Fee php bin/magento setup:upgrade php bin/magento setup:static-content:deploy -f php bin/magento cache:flush |
Frontend Screenshots
1) View and Edit Cart Page
The fee shows clearly in the cart's order total summary:
2) Checkout Page
The fee is included in the checkout summary:
3) Customer Account — My Orders Page
Customers can see the fee in their order history under "My Orders":
Conclusion
That's the full implementation: a total collector registered in sales.xml, frontend components for the cart and checkout, and a block for order views — enough to charge and display a custom fee across the whole order lifecycle.
Keep in mind what this walkthrough doesn't cover: a hardcoded fee with no admin configuration, no conditions (customer group, cart contents, payment method), and no handling in invoices or credit memos — and as a custom total collector, it's yours to re-verify on every Magento upgrade. If you'd rather manage all of that from the admin panel, the Magento 2 Extra Fee extension handles it out of the box: fixed or percentage fees, multiple fees per order, flexible conditions, and correct totals across orders, invoices, and refunds — no code changes required.