Setting Advanced Shipping Restrictions in Magento 2

3
13830
Reading Time: 6 minutes

In this article, we’ll explain how to hide a shipping method when certain conditions get applied.

In some scenarios store owners may want to restrict shipping options because:

  • a product has big dimensions and weight, hence it can’t be shipped in any standard way,
  • frozen storage holding conditions don’t allow to deliver a product in time
  • shipping is restricted due to the customer’s location (country, region, city)
  • the Cart Total doesn’t allow a shopper to qualify for any shipping offers,
  • etc.

The default selection of Magento shipping options doesn’t offer much flexibility. It’s only possible to restrict shipping for certain counties and regions or when the Cart Total is less than the set amount.

If you think that this is not enough, read on.

I’ll tell you how to create your own plugin that will let you restrict shipping options according to your business model, goals and conditions.

NOTE

I won’t go through the specific steps necessary to create an extension for Magento 2. You can see how to do that here or here.

In order to dynamically disable any given shipping method, we need a new set of classes in our extension – these are 2 plugins. The 1st will be in charge of shipping method validation, while the 2nd will be filtering them.

In a nutshell, every method that doesn’t pass validation will get marked as “is_disabled“, and at the next step, all the “is_disabled” methods get filtered and deleted from the main list.

Let’s roll.

* Since the approach I’m going to describe is based on the principles used in the Shipping Suite extension, I’ll be using the name of the vendor ‘Mageworx’ and the name of the extension ‘ShippingRules’. NOTE! In order to avoid errors, you need to replace those with your own names! 

First, let’s create a plugin (class) that will be used for filtering. Here is how to:

> MageWorx\ShippingRules\Model\Plugin\Shipping\Rate\Result\Append
namespace MageWorx\ShippingRules\Model\Plugin\Shipping\Rate\Result;

class Append
{
    /**
     * @var \Magento\Checkout\Model\Session|\Magento\Backend\Model\Session\Quote
     */
    protected $session;

    /**
     * @param \Magento\Checkout\Model\Session $checkoutSession
     * @param \Magento\Backend\Model\Session\Quote $backendQuoteSession
     * @param \Magento\Framework\App\State $state
     * @internal param Session $session
     */
    public function __construct(
        \Magento\Checkout\Model\Session $checkoutSession,
        \Magento\Backend\Model\Session\Quote $backendQuoteSession,
        \Magento\Framework\App\State $state
    ) {
        if ($state->getAreaCode() == \Magento\Framework\App\Area::AREA_ADMINHTML) {
            $this->session = $backendQuoteSession;
        } else {
            $this->session = $checkoutSession;
        }
    }

    /**
     * Validate each shipping method before append.
     * Apply the rules action if validation was successful.
     * Can mark some rules as disabled. The disabled rules will be removed in the class
     * @see MageWorx\ShippingRules\Model\Plugin\Shipping\Rate\Result\GetAllRates
     * by checking the value of this mark in the rate object.
     *
     * NOTE: If you have some problems with the rules and the shipping methods, start debugging from here.
     *
     * @param \Magento\Shipping\Model\Rate\Result $subject
     * @param \Magento\Quote\Model\Quote\Address\RateResult\AbstractResult|\Magento\Shipping\Model\Rate\Result $result
     * @return array
     */
    public function beforeAppend($subject, $result)
    {
        if (!$result instanceof \Magento\Quote\Model\Quote\Address\RateResult\Method) {
            return [$result];
        }

        $filtableMethods = [
            'flatrate_flatrate',
            'ups_XDM',
            'ups_XPR',
            'ups_WXS',
            'carrier_method',
            // ... add here your method codes
        ];
        $methodCode = $result->getCarrier() . '_' . $result->getMethod();
        if (!in_array($methodCode, $filtableMethods)) {
            return [$result];
        }

        /** @var \Magento\Quote\Model\Quote $quote */
        $quote = $this->session->getQuote();
        $quoteItems = $quote->getAllItems();
        $heavyWeightFlag = false;
        foreach ($quoteItems as $item) {
            if ($item->getWeight() > 100) {
                $heavyWeightFlag = true;
                continue;
            }
        }

        if ($heavyWeightFlag == true) {
            $result->setIsDisabled(true);
        }

        return [$result];
    }
}

We run 3 checks in the body of our plugin:

1.

!$result instanceof \Magento\Quote\Model\Quote\Address\RateResult\Method

– ensures that at the point of entry we get what we need: that is an instance of a shipping method. In case it is not, we just return it as it is.

2.

!in_array($methodCode, $filtableMethods)

– next, we check whether the current method in on the list of methods for filtering. In case it is not, we return it as it is.

3.

$heavyWeightFlag == true

– is the last and the main check. If the flag is applied to the real value, the current method gets disabled: $result->setIsDisabled(true);

We’ll try to filter at least one product added to cart and having weight more than 100 standard units (pounds or kilos).
The methods, available for filtering are ‘flatrate_flatrate’,’ups_XDM’,’ups_XPR’,’ups_WXS’,’carrier_method’. They are hard-coded and stored in the array $filtableMethods.

We operate on the code using the entire shipping method code which, as a rule, consists of the carrier’s code ($result->getCarrier()) and the code of the method itself ($result->getMethod()) connected with “_”.

Products needed for the check are taken directly from the current client’s quote.
NOTE

A little piece of code in the array class constructor will let you define the quote both on the frontend and in the backend (providing an order was created in the Admin panel).

Using \Magento\Framework\App\State we can check in which area we are at the moment, and select the necessary quote:
1. \Magento\Backend\Model\Session\Quote for Admin
2. \Magento\Checkout\Model\Session for the Frontend

That’s it!

Now we can validate shipping methods and can proceed to the next stage – creating a plugin for disabling invalid (according to our conditions) shipping methods.

This is how to:

> MageWorx\ShippingRules\Model\Plugin\Shipping\Rate\Result\GetAllRates
/**
 * Copyright © 2016 MageWorx. All rights reserved.
 * See LICENSE.txt for license details.
 */

namespace MageWorx\ShippingRules\Model\Plugin\Shipping\Rate\Result;

class GetAllRates
{

    /**
     * Disable the marked shipping rates.
     *
     * NOTE: If you can not see some of the shipping rates, start debugging from here. At first, check 'is_disabled'
     * param in the shipping rate object.
     *
     * @param \Magento\Shipping\Model\Rate\Result $subject
     * @param array $result
     * @return array
     */
    public function afterGetAllRates($subject, $result)
    {
        foreach ($result as $key => $rate) {
            if ($rate->getIsDisabled()) {
                unset($result[$key]);
            }
        }

        return $result;
    }
}

This plugin is used to fetch all available shipping methods data and check each of them. If some of the methods has the “is_disabled” mark, we just exclude it from the list, which enables us to see only those methods that correspond to the conditions  created in the validator. These methods get excluded on any page of a website, be it an estimate shipping block in shopping cart, or the checkout page.

Now, to check how out plugins work, we need to register them in the file:

> MageWorx/ShippingRules/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Shipping\Model\Rate\Result">
        <plugin name="mageworx_shippingrules_update_rate_result"
                type="MageWorx\ShippingRules\Model\Plugin\Shipping\Rate\Result\Append"
                sortOrder="10"
                disabled="false"/>
        <plugin name="mageworx_shippingrules_update_disabled_or_enabled_rates"
                type="MageWorx\ShippingRules\Model\Plugin\Shipping\Rate\Result\GetAllRates"
                sortOrder="11"
                disabled="false"/>
    </type>
</config>

NOTE

For descriptive reasons, the plugins are divided into 2 different classes. In an ordinary scenario, you can use just 1 class to modify 2 different methods of the class Magento\Shipping\Model\Rate\Result.

Why it works this way?

Magento\Shipping\Model\Rate\Result is the main Magento 2 class that is responsible for shipping methods procession. Its getAllRates method is used to fetch shipping method lists.

Meanwhile, adding shipping methods is usually done with the help of the ‘append’ method. Having validated methods while appending, and having excluded them from the collection, we can get a list of valid methods.

Validation itself can also be modified.

Our extension has a big special class that stores a collection of custom filters with different conditions (such as  store_id, customer_group_id, date from/to, days of the week, etc.). it also has a separate class with a collection of actions that should be applied to a shipping method (e.g. enable or disable it, rewrite its cost according to different conditions, add extra costs or discounts, etc.)

The result of the 1st plugin in debug looks somewhat like this (for a product with the weight of 1 unit):
1

This is how it looks in shopping cart (ups_WXS is still available, as the weight limit is not exceeded):

2

However, if you change the product weight to 101, this shipping method becomes unavailable:

3

It gets available again if you excluded it from the list:
4

5

At the checkout:

f3d3cac03678318af2d569d757fc7e79

NOTE

You can get the list of all available system shipping methods right in the code the following way:

/**
 * Return array of carriers.
 * If $isActiveOnlyFlag is set to true, will return only active carriers
 *
 * @param bool $isActiveOnlyFlag
 * @return array
 */
public function getAvailableMethods($isActiveOnlyFlag = false)
{
    $carriers = $this->shippingConfig->getAllCarriers();
    foreach ($carriers as $carrierCode => $carrierModel) {
        if (!$carrierModel->isActive() && (bool)$isActiveOnlyFlag === true) {
            continue;
        }
        $carrierMethods = $carrierModel->getAllowedMethods();
        if (!$carrierMethods) {
            continue;
        }
        foreach ($carrierMethods as $methodCode => $methodTitle) {
            $methods[] = $carrierCode . '_' . $methodCode;
        }
    }

    return !empty($methods) ? $methods : [];
}

Where `$this->shippingConfig` is an instance of the class `Magento\Shipping\Model\Config`. Using this code, you can create the source model for the selector of shipping methods in the extension’s settings (thus, you won’t have to hard-code it in the plugin).

And last but not least.

If you don’t want to create your own extension for disabling shipping methods, you can use our Shipping Suite solution for Magento 2.

The extension lets you exclude any shipping method whenever needed just in a couple of mouse clicks.
1. Create a new shipping rule with the following condition:

8

2. Select Hide Shipping Method action on the list, and choose which methods should be disabled:

9

NOTE

If you don’t find the ‘weight’ attribute on the list of conditions, check is it is enabled to use in promo rules:

10

Should you have any questions or ideas on how to further improve our extension, please share your opinion in the comments section below.

I am a huge coffee fan. If I’m not drinking it, I’m likely to be busy with getting MageWorx projects done. Fond of reading sci-fi books (especially those about dwarfs, ogres and the post-apocalyptical world). My biggest dream is to find a huge chest of gold and buy my own uninhabited island. Happy husband. Proud father. Ah... and also I'm a certified Magento developer. ;)

3 COMMENTS

  1. very, very good. Searched for days for this. My website uses your customoptions, could not live without. We are currently on Magento 1. Im playing around converting to 2 and assume eventually will get your customoption for 2. I converted enough so that I can convert the database and play.

LEAVE A REPLY

Please enter your comment!
Please enter your name here