I want to add a custom fee to the cart summary when the checkbox is check and remove when unchecked and also adjusting the order total with each checkbox change. How can I achieve this?
1 Answer
I had to do the same but on the checkout summary. I hope this can help you. since my checkout is one step checkout because i'm using a template I have decided to create a new step
To have our values dynamically, I created the fields in the admin etc/adminhtml/system.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="config_bag_checkout" translate="label" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
<class>separator-top</class>
<label>Configuration</label>
<tab>xxxxxxxx</tab>
<resource>xxxxxxxx::erp</resource>
<group id="customfee" translate="label" type="text" sortOrder="500" showInDefault="1" showInWebsite="1"
showInStore="1">
<label>Bag Fee Settings</label>
<field id="status" translate="label comment" type="select" sortOrder="1" showInDefault="1"
showInWebsite="1" showInStore="1">
<label>Status</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="bagfee_amount" translate="label comment" type="text" sortOrder="3" showInDefault="1"
showInWebsite="1" showInStore="1">
<label>Bag Fee Amount</label>
<validate>validate-number validate-zero-or-greater</validate>
</field>
<field id="minimum_order_amount" translate="label comment" type="text" sortOrder="4" showInDefault="1"
showInWebsite="1" showInStore="1">
<label>Minimum Order Amount</label>
<comment><![CDATA[Greaterthan or equal to]]></comment>
<validate>validate-number validate-zero-or-greater</validate>
</field>
</group>
</section>
</system>
</config>
Then create a helper to get the data from the admin Helper/Data.php
<?php
namespace Vendor\Module\Helper;
use Magento\Framework\App\Helper\AbstractHelper;
class Data extends AbstractHelper {
/**
* Bag fee config path
*/
const CONFIG_CUSTOM_IS_ENABLED = 'config_bag_checkout/customfee/status';
const CONFIG_CUSTOM_FEE = 'config_bag_checkout/customfee/bagfee_amount';
const CONFIG_MINIMUM_ORDER_AMOUNT = 'config_bag_checkout/customfee/minimum_order_amount';
/**
* @return mixed
*/
public function isModuleEnabled() {
$storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
$isEnabled = $this->scopeConfig->getValue(self::CONFIG_CUSTOM_IS_ENABLED, $storeScope);
return $isEnabled;
}
/**
* Get bag fee
*
* @return mixed
*/
public function getBagFee() {
$storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
$fee = $this->scopeConfig->getValue(self::CONFIG_CUSTOM_FEE, $storeScope);
return $fee;
}
/**
* @return mixed
*/
public function getMinimumOrderAmount() {
$storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
$MinimumOrderAmount = $this->scopeConfig->getValue(self::CONFIG_MINIMUM_ORDER_AMOUNT, $storeScope);
return $MinimumOrderAmount;
}
}
To trigger the class that is in charge of updating the prices we must create a sales.xml
file inside etc folder
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd">
<section name="quote">
<group name="totals">
<item name="customfee" instance="Vendor\Module\Model\Total\Customfee" sort_order="150"/>
</group>
</section>
</config>
Then create the file Model/Total/Customfee.php
<?php
namespace Vendor\Module\Model\Total;
class Customfee extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal {
/**
* @var \Vendor\Module\Helper\Data
*/
protected $dataHelper;
/**
* Collect grand total address amount
*
* @param \Magento\Quote\Model\Quote $quote
* @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment
* @param \Magento\Quote\Model\Quote\Address\Total $total
* @return $this
*/
protected $quoteValidator = null;
public function __construct(
\Magento\Quote\Model\QuoteValidator $quoteValidator,
\Vendor\Module\Helper\Data $dataHelper
) {
$this->dataHelper = $dataHelper;
$this->quoteValidator = $quoteValidator;
}
public function collect(
\Magento\Quote\Model\Quote $quote,
\Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment,
\Magento\Quote\Model\Quote\Address\Total $total
) {
parent::collect($quote, $shippingAssignment, $total);
$data = [];
$bssData = $quote->getBssCustomfield();
if ($bssData) {
$data = json_decode($bssData);
}
if (isset($data->bag_option) && $data->bag_option === "true") {
$bagPrice = $this->dataHelper->getBagFee();
$minimumOrderAmount = $this->dataHelper->getMinimumOrderAmount();
$bagFee = $total->getBaseSubtotal() <= $minimumOrderAmount ? $this->dataHelper->getBagFee(): 0;
$total->setTotalAmount('customfee', $bagFee);
$total->setBaseTotalAmount('customfee', $bagFee);
$total->setCustomFee($bagFee);
$total->setGrandTotal($total->getGrandTotal());
$total->setBaseGrandTotal($total->getBaseGrandTotal());
} else {
$bagFee = 0;
$total->setTotalAmount('customfee', $bagFee);
$total->setBaseTotalAmount('customfee', $bagFee);
$total->setCustomFee($bagFee);
$total->setGrandTotal($total->getGrandTotal());
$total->setBaseGrandTotal($total->getBaseGrandTotal());
}
return $this;
}
protected function clearValues(Address\Total $total) {
$total->setTotalAmount('subtotal', 0);
$total->setBaseTotalAmount('subtotal', 0);
$total->setTotalAmount('tax', 0);
$total->setBaseTotalAmount('tax', 0);
$total->setTotalAmount('discount_tax_compensation', 0);
$total->setBaseTotalAmount('discount_tax_compensation', 0);
$total->setTotalAmount('shipping_discount_tax_compensation', 0);
$total->setBaseTotalAmount('shipping_discount_tax_compensation', 0);
$total->setSubtotalInclTax(0);
$total->setBaseSubtotalInclTax(0);
}
/**
* @param \Magento\Quote\Model\Quote $quote
* @param Address\Total $total
* @return array|null
*/
/**
* Assign subtotal amount and label to address object
*
* @param \Magento\Quote\Model\Quote $quote
* @param Address\Total $total
* @return array
*/
public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total) {
return [
'code' => 'customfee',
'title' => 'Custom Fee',
'value' => $this->dataHelper->getBagFee(),
];
}
/**
* Get Subtotal label
*
* @return \Magento\Framework\Phrase
*/
public function getLabel() {
return __('Custom Fee');
}
}
In my case I have installed a bss module called Checkout Custom Field this is how I validate if the user has already checked the box or not but you can replace this to use a checkoutSession
instead of my implementation. any will work, the important thing is to know when the box is checked or unchecked to apply the fee.
Now we need to create a controller, this is called when the user selects the checkbox to save the value.
etc/frontend/routes.xml
<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route frontName="customFee" id="customFee">
<module name="Vendor_Module"/>
</route>
</router>
</config>
Controller/Index/Index.php
<?php
namespace Vendor\MOdule\Controller\Index;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Quote\Model\QuoteIdMaskFactory;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Framework\Controller\Result\JsonFactory;
class Index extends Action {
private $quoteIdMaskFactory;
/** @var \Magento\Framework\Controller\Result\Json $result */
protected $resultJsonFactory;
/**
* @var CartRepositoryInterface
*/
protected $cartRepository;
public function __construct(
Context $context,
JsonFactory $resultJsonFactory,
QuoteIdMaskFactory $quoteIdMaskFactory,
CartRepositoryInterface $cartRepository
) {
$this->cartRepository = $cartRepository;
$this->resultJsonFactory = $resultJsonFactory;
$this->quoteIdMaskFactory = $quoteIdMaskFactory;
parent::__construct($context);
}
public function execute() {
$result = $this->resultJsonFactory->create();
$resultRedirect = $this->resultRedirectFactory->create();
$post = $this->getRequest()->getPostValue();
$resultRedirect->setPath('checkout/cart');
if (!$post) {
return $resultRedirect;
}
try {
$maskedQuoteId = $post['quote_id'];
$customFeeCheck = $post['enable_custom_fee'];
$quoteIdMask = $this->quoteIdMaskFactory->create()->load($maskedQuoteId, 'masked_id');
$quoteId = $quoteIdMask->getQuoteId();
$quote = $this->cartRepository->get($quoteId);
$quote->setBssCustomfield(json_encode(["bag_option" => $customFeeCheck]));
$quote->save();
return $result->setData(['success' => $quote->getData()]);
} catch (\Exception $e) {
return $result->setData(['error' => $e->getMessage()]);
}
}
}
This controller receives two attributes the quoteId and whether the field is checked or unchecked. i use the quote.
In my case i use the quote because there is the information of the purchase and I set the field if it is check or not. here there are two ways to set the information here, do not use the quote and use a checkoutSession
or see what information you have in the quote that you can set and then use it in the Model/Total/Customfee.php
. to know which data you have on the quote do this $quote->getData()
(you can see if you can reuse something or not)
Now let's dig into the UI As I mentioned before, I've created a new step for my input check box
file /view/frontend/web/js/view/bag-step.js
define([
'jquery',
'ko',
'uiComponent',
'Magento_Checkout/js/model/step-navigator',
'Magento_Checkout/js/model/quote',
'Magento_Checkout/js/model/totals',
'Magento_Checkout/js/model/cart/totals-processor/default',
'Magento_Catalog/js/price-utils'
], function ($, ko, Component, stepNavigator, quote, totals, totalsDefaultProvider, priceUtils) {
'use strict';
var bagPrice = window.checkoutConfig.bagFeeSettings.fee_amount;
var minimumFee = window.checkoutConfig.bagFeeSettings.minimum_order_amount;
return Component.extend({
defaults: {
template: 'Vendor_Module/bag-step'
},
stepCode: 'bag',
errorValidationMessage: ko.observable(false),
isCustomFeeCheck: ko.observable(false),
initialize: function () {
this._super();
var self = this;
if (!parseInt(window.checkoutConfig.bagFeeSettings.show_hide_block)) {
$(".checkout-bag-step.hoverable").hide();
}
var bss_customfield = JSON.parse(window.checkoutConfig.quoteData.bss_customfield)
if (bss_customfield && bss_customfield.bag_option === 'true') {
this.isCustomFeeCheck(true);
}
totals.totals.subscribe(function(data) {
if (data.base_subtotal <= minimumFee) {
self.getFeeAmount(priceUtils.formatPrice(bagPrice, quote.getPriceFormat()));
} else {
self.getFeeAmount(priceUtils.formatPrice(0, quote.getPriceFormat()));
}
});
return this;
},
getFeeAmount: ko.observable(priceUtils.formatPrice(bagPrice, quote.getPriceFormat())),
getMinimumFee: priceUtils.formatPrice(minimumFee, quote.getPriceFormat()),
handleCheckbox: function () {
$.ajax({
url: `/${checkoutConfig.storeCode}/customFee/index/index`,
method: 'POST',
beforeSend: function() {
totals.isLoading(true);
},
data: {
quote_id: quote.getQuoteId(),
enable_custom_fee: this.isCustomFeeCheck(),
},
dataType: 'json',
success: function () {
totalsDefaultProvider.estimateTotals(quote.shippingAddress());
},
error: function (xhr, status, error) {
console.log('Error: ' + error);
}
}).always(function() {
totals.isLoading(false);
});
return true;
}
});
});
Take a look to the handleCheckbox
method, there is the key:
- We call the controller we have just created.
- The controller set the data
- onSuccess call
totalsDefaultProvider.estimateTotals(quote.shippingAddress());
estimateTotals
trigger theModel/Total/Customfee.php
fileCustomfee.php
file validates whether the box is checked or uncheckedif (isset($data->bag_option) && $data->bag_option === "true") {
base on that applies the fee or not- Magento automatically updates prices in the checkout summary
Please note that I have a lot of bss dependencies but you can replace this with a session hope this helps.