Magento 2 - How to add custom free shipping based on cart conditions and hide other methods if applicable
In this article we are going to create a custom shipping method which will shown only once the cart grand total value reaches over a configured value. In addition we will also add functionality that allows hiding other methods if the new one is available. In order to give this a bit of business meaning we will create the method as a custom free shipping method. This can turn out to be useful in case you already use the stock method for something else and do not want to handle cart rules. Lets get started.
First you will need to create a Magento 2 module. Ours is going to be called Techflarestudio_FreeShipping. Once you have created the module the first step is to define system configuration for our new shipping method. We can do this in system.xml just as any other configuration file. Only in this case we will add a new group under carriers section.
Lots of things to break down here:
- We are defining a yes/no field which will allow the store admin to enable or disable the method
- We are defining a title, name, cost which are self explanatory
- We are also setting the in-built Ship to Applicable/Specifi Countries which will allow the store admin to configure the method to work for only certain shipping addresses
- Sort order allows us to define the methods position in regards to other in shipping step
- We are also adding a custom field Subtotal Threshold. This will allow the store admin to define the cart subtotal value which must be reached in order to allow using this method
Once we have the configuration set we need to add default values. We can do this in config.php.
This is simply setting the default values for the configuration. However one important thing to note here is the model which we have set to use a custom carrier. Lets go on and create the carrier model.
<?php
/*
* @category Techflarestudio
* @author Wasalu Duckworth
* @copyright Copyright (c) 2021 Techflarestudio, Ltd. (https://techflarestudio.com)
* @license http://opensource.org/licenses/OSL-3.0 The Open Software License 3.0 (OSL-3.0)
*
*/
namespace Techflarestudio\FreeShipping\Model\Carrier;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory;
use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Rate\ResultFactory;
use Magento\Checkout\Model\Session as CheckoutSession;
/**
* Class Freeshipping
* @package Techflarestudio\FreeShipping\Model\Carrier
*/
class FreeShipping extends AbstractCarrier implements CarrierInterface
{
/**
* @var string
*/
protected $_code = 'custom_free_shipping';
/**
* @var bool
*/
protected $_isFixed = true;
/**
* @var TotalsInterface
*/
protected $_cartTotalRepository;
/**
* @var CheckoutSession
*/
protected $_checkoutSession;
/**
* @var ResultFactory
*/
private $rateResultFactory;
/**
* @var MethodFactory
*/
private $rateMethodFactory;
/**
* @param ScopeConfigInterface $scopeConfig
* @param ErrorFactory $rateErrorFactory
* @param \Psr\Log\LoggerInterface $logger
* @param ResultFactory $rateResultFactory
* @param MethodFactory $rateMethodFactory
* @param TotalsInterface $_cartTotalRepository
* @param CheckoutSession $_checkoutSession
* @param array $data
*/
public function __construct(
ScopeConfigInterface $scopeConfig,
ErrorFactory $rateErrorFactory,
\Psr\Log\LoggerInterface $logger,
ResultFactory $rateResultFactory,
MethodFactory $rateMethodFactory,
TotalsInterface $_cartTotalRepository,
CheckoutSession $_checkoutSession,
array $data = []
) {
parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
$this->_cartTotalRepository = $_cartTotalRepository;
$this->_checkoutSession = $_checkoutSession;
$this->rateResultFactory = $rateResultFactory;
$this->rateMethodFactory = $rateMethodFactory;
}
/**
* Custom Shipping Rates Collector
*
* @param RateRequest $request
* @return \Magento\Shipping\Model\Rate\Result|bool
*/
public function collectRates(RateRequest $request)
{
if (!$this->getConfigFlag('active') || !$this->validateSubtotal()) {
return false;
}
/** @var \Magento\Shipping\Model\Rate\Result $result */
$result = $this->rateResultFactory->create();
/** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
$method = $this->rateMethodFactory->create();
$method->setCarrier($this->_code);
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod($this->_code);
$method->setMethodTitle($this->getConfigData('name'));
$shippingCost = (float)$this->getConfigData('shipping_cost');
$method->setPrice($shippingCost);
$method->setCost($shippingCost);
$result->append($method);
return $result;
}
/**
* @return array
*/
public function getAllowedMethods()
{
return [$this->_code => $this->getConfigData('name')];
}
/**
* @return bool
*/
protected function validateSubtotal()
{
$subtotalThreshold = (float)$this->getConfigData('subtotal_threshold');
$subtotal = $this->getSubtotal();
if(!$subtotalThreshold || !$subtotal) {
return false;
}
if($subtotalThreshold > $subtotal) {
return false;
}
return true;
}
/**
* @return false|float
*/
protected function getSubtotal()
{
try {
return $this->_checkoutSession->getQuote()->getSubtotal();
} catch (NoSuchEntityException $e) {
$this->_logger->critical($e);
} catch (LocalizedException $e) {
$this->_logger->critical($e);
}
return false;
}
}
This is a fairly standard shipping carrier model. As you can see there are 2 conditions which can make the method available in checkout. Both of these happen in collectRates function.
- First we are checking if the carrier is enabled in configuration
- Secondly we are fetching the current quote subtotal value and comparing that to the configured threshold value \Techflarestudio\FreeShipping\Model\Carrier\FreeShipping::validateSubtotal
- The tricky part here is to access the quote data. We do this by injecting Magento\Checkout\Model\Session which gives us access to current session and cart contents.
Go to store admin - Stores->Configuration->Sales->Delivery Methods->Custom Free Shipping. You should be able to see something similar:
Lets configure the threshold to be 50$. Enable the method, set all of the required fields, flush the cache and go to checkout. Make sure to add more than 50$ to cart before getting there - the subtotal validation will filter out the method otherwise. We should be able to see our newly defined custom shipping method.
This is great. We have already added a new shipping method which the store admin can configure based on subtotal threshold and set a custom price for the method.
Next we will add functionality that will allow to hide other methods in case our custom free shipping is available. To do so we will need to hook into the shipping method functionality and filter the methods before rendering.
When entering the address Magento is loading the following route rest/default/V1/guest-carts/../estimate-shipping-methods.
If we look up the route definition we can see that it is defined like this:
So we can see that this function is using the following service class Magento\Quote\Api\GuestShipmentEstimationInterface. Following the login in estimateByExtendedAddress we can see that the parent logic is actually defined here:
The getShippingMethods function is a private method that returns a ShippingMethodInterface[]. This is a perfect place to create a plugin for estimateByExtendedAddress function. We will hook into the function after it returns the list and do our custom modifications to the output array.
To create the plugin we first need to define a plugin in xml.
Next we will define the plugin class and the after method.
A few things to note here:
- In our example we have hard-coded the allowed method code. We could easily create a system configuration field which would allow to both enable/disable this filter + allow to configure which methods are allowed to be shown when this is enabled.
That is it. Make sure to refresh the cache, generate the interceptors and you should be able to see only the custom free shipping valid on checkout.