Discounts are still one of the most reliable ways to nudge a hesitant shopper into completing an order. Magento 2 ships with cart price rules that cover the common cases, but every store eventually hits a promotion the rule engine simply can't express — a fixed adjustment that has to be applied directly to the order totals.
That's when you add a custom discount programmatically. In this guide, we'll register a custom total collector, apply the discount to the quote, and display it on both the cart and checkout pages using Magento's knockout components.
Before you start, you'll need a working module (we're using Mageants_Blog here — replace the vendor and module name with your own) and admin access to run CLI commands after deployment.
Tip: If what you actually want to run is a promotion — Buy X Get Y, or a free product added to the cart when the subtotal crosses a threshold — you don't need custom code at all. The Magento 2 Free Gift extension auto-adds free products to the cart by subtotal, SKU, or coupon, with no total collectors to maintain.
Steps to Add A Custom Discount Programmatically in Magento 2
Step 1 : Register the custom total in sales.xml
Magento collects order totals (subtotal, shipping, tax, and so on) through registered collectors. The sales.xml file tells Magento that our module adds one more collector to that chain. Create it at:
app/code/Mageants/Blog/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="customdiscount" instance="Mageants\Blog\Model\Total\Quote\Custom" sort_order="400"/>
</group>
</section>
</config>
|
Step 2 : Apply the discount in the total collector model
The model referenced above does the actual work: its collect() method subtracts the discount from the grand total. In this example the discount is a fixed $10 (see $baseDiscount = 10) — in a real project you'd load this value from configuration or calculate it from your own business logic. Note that we convert the base amount into the display currency with PriceCurrencyInterface, so multi-currency stores show the right figure. Create the file at:
app/code/Mageants/Blog/Model/Total/Quote/Custom.php
<?php
namespace Mageants\Blog\Model\Total\Quote;
/**
* @param \Magento\Quote\Model\Quote $quote
* @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment
* @param \Magento\Quote\Model\Quote\Address\Total $total
* @return $this|bool
*/
use Magento\Quote\Model\Quote;
use Magento\Quote\Api\Data\ShippingAssignmentInterface;
use Magento\Quote\Model\Quote\Address\Total;
class Custom extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal
{
/**
* @var \Magento\Framework\Pricing\PriceCurrencyInterface
*/
protected $_priceCurrency;
/**
* Custom constructor.
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
*/
public function __construct(
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
){
$this->_priceCurrency = $priceCurrency;
}
public function collect(
Quote $quote,
ShippingAssignmentInterface $shippingAssignment,
Total $total
)
{
parent::collect($quote, $shippingAssignment, $total);
$baseDiscount = 10;
$discount = $this->_priceCurrency->convert($baseDiscount);
$total->addTotalAmount('customdiscount', -$discount);
$total->addBaseTotalAmount('customdiscount', -$baseDiscount);
$total->setBaseGrandTotal($total->getBaseGrandTotal() - $baseDiscount);
$quote->setCustomDiscount(-$discount);
return $this;
}
}
|
At this point the grand total already changes — but the discount line itself is invisible, because Magento renders the totals block with Knockout JS. The remaining steps add that display, first on the cart page (steps 3–5) and then on the checkout page (steps 6–8).
Step 3 : Add the total to the cart page layout
app/code/Mageants/Blog/view/frontend/layout/checkout_cart_index.xml
<?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.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="customdiscount" xsi:type="array">
<item name="component" xsi:type="string">Mageants_Blog/js/view/checkout/cart/totals/customdiscount</item>
<item name="sortOrder" xsi:type="string">20</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Mageants_Blog/checkout/cart/totals/customdiscount</item>
<item name="customdiscount" xsi:type="string" translate="true">Custom Discount</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
|
Step 4 : Create the Knockout view model for the cart
app/code/Mageants/Blog/view/frontend/web/js/view/checkout/cart/totals/customdiscount.js
define(
[
'jquery',
'Magento_Checkout/js/view/summary/abstract-total',
'Magento_Checkout/js/model/quote',
'Magento_Checkout/js/model/totals',
'Magento_Catalog/js/price-utils'
],
function ($,Component,quote,totals,priceUtils) {
"use strict";
return Component.extend({
defaults: {
template: 'Mageants/Blog/checkout/cart/totals/customdiscount'
},
totals: quote.getTotals(),
isDisplayedCustomdiscountTotal : function () {
return true;
},
getCustomdiscountTotal : function () {
var price = - 10;
return this.getFormattedPrice(price);
}
});
}
);
|
Also Read: How To Add A Custom Filter To The Product Grid In Magento 2?
Step 5 : Add the Knockout template for the cart totals
app/code/Mageants/Blog/view/frontend/web/template/checkout/cart/totals/customdiscount.html
<!-- ko if: isDisplayedCustomdiscountTotal() -->
<tr class="border border-gray">
<th class="mark" colspan="1" scope="row" data-bind="text: customdiscount"></th>
<td class="amount">
<span class="price" data-bind="text: getCustomdiscountTotal(), attr: {'data-th': customdiscount}"></span>
</td>
</tr>
<!-- /ko -->
|
Step 6 : Add the total to the checkout page layout
The checkout summary uses a deeper jsLayout tree than the cart, so the same component is registered under the sidebar summary totals. Create the file at:
app/code/Mageants/Blog/view/frontend/layout/checkout_index_index.xml
<?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="customdiscount" xsi:type="array">
<item name="component" xsi:type="string">
Mageants_Blog/js/view/checkout/summary/customdiscount
</item>
<item name="sortOrder" xsi:type="string">20</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Mageants_Blog/checkout/summary/customdiscount</item>
<item name="title" xsi:type="string" translate="true">Custom Discount</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>
|
Step 7 : Create the Knockout view model for the checkout summary
app/code/Mageants/Blog/view/frontend/web/js/view/checkout/summary/customdiscount.js
define(
[
'jquery',
'Magento_Checkout/js/view/summary/abstract-total',
'Magento_Checkout/js/model/quote',
'Magento_Checkout/js/model/totals',
'Magento_Catalog/js/price-utils'
],
function ($,Component,quote,totals,priceUtils) {
"use strict";
return Component.extend({
defaults: {
template: 'Mageants/Blog/checkout/summary/customdiscount'
},
totals: quote.getTotals(),
isDisplayedCustomdiscountTotal : function () {
return true;
},
getCustomdiscountTotal : function () {
var price = - 10;
return this.getFormattedPrice(price);
}
});
}
);
|
Step 8 : Add the Knockout template for the checkout summary
app/code/Mageants/Blog/view/frontend/web/template/checkout/summary/customdiscount.html
<!-- ko if: isDisplayedCustomdiscountTotal() -->
<tr class="border border-gray">
<th class="mark" colspan="1" scope="row" data-bind="text: title"></th>
<td class="amount">
<span class="price" data-bind="text: getCustomdiscountTotal()"></span>
</td>
</tr>
<!-- /ko -->
|
Finally, deploy the changes and flush the cache:
php bin/magento setup:upgrade php bin/magento setup:static-content:deploy -f php bin/magento cache:flush |
You should now see the custom discount line on both the cart and checkout pages:
Custom code or an extension — which should you use?
The approach above is the right tool when you need a fixed, rule-independent adjustment on order totals and you have a developer to maintain it through Magento upgrades. But if the discount is really a promotion — gifting a product on a minimum cart value, running Buy X Get Y offers, or letting customers combine coupon codes — custom total collectors quickly become hard to maintain. For those cases, a ready-made module such as our free gift with purchase extension for Magento 2 or the Magento 2 Multiple Coupons extension gives store owners full control from the admin panel, with no code changes on every promotion.
Conclusion
That's all it takes to add a custom discount programmatically in Magento 2: register the total collector in sales.xml, apply the amount in collect(), and wire up the Knockout components so the discount is visible on the cart and checkout pages. If you run into any issue with the implementation, contact us or let us know in the comments — and if you'd rather skip the custom code entirely, the Free Gift extension handles automatic free-product promotions out of the box.