Skip to content
advanced Phase 90 · Shipping Deep Dive

Custom Shipping Carriers

Custom shipping carriers - carrier model, rate model, tracking, configuration

1h
0 problems
Topic Progress 0%

Custom Carrier Model

Complete Carrier Implementation

namespace Vendor\Shipping\Model\Carrier;

use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Rate\Result;
use Magento\Quote\Model\Quote\Address\RateResult\Factory as RateResultFactory;
use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory as RateMethodFactory;

class CustomCarrier extends AbstractCarrier implements CarrierInterface
{
    protected $_code = 'customcarrier';
    protected $_isFixed = false;

    private RateResultFactory $rateResultFactory;
    private RateMethodFactory $rateMethodFactory;
    private Config $carrierConfig;

    public function __construct(
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        RateResultFactory $rateResultFactory,
        RateMethodFactory $rateMethodFactory,
        Config $carrierConfig,
        array $data = []
    ) {
        $this->rateResultFactory = $rateResultFactory;
        $this->rateMethodFactory = $rateMethodFactory;
        $this->carrierConfig = $carrierConfig;
        parent::__construct($scopeConfig, $data);
    }

    public function collectRates(\Magento\Shipping\Model\Request\Data $request): Result
    {
        $result = $this->rateResultFactory->create();

        if (!$this->getConfigData('active')) {
            return $result;
        }

        $this->carrierConfig->load($request);

        $methods = $this->carrierConfig->getMethods();
        foreach ($methods as $methodCode => $methodConfig) {
            if (!$this->isMethodAvailable($methodCode, $request)) {
                continue;
            }

            $rate = $this->rateMethodFactory->create();
            $rate->setCarrier($this->_code);
            $rate->setMethod($methodCode);
            $rate->setCarrierTitle($this->getConfigData('title'));
            $rate->setMethodTitle($methodConfig['title']);
            $rate->setPrice($this->calculateRate($methodCode, $request));
            $rate->setCost($this->calculateCost($methodCode, $request));

            $result->append($rate);
        }

        return $result;
    }

    public function isAvailable(\Magento\Quote\Model\Quote\Address $request): bool
    {
        return $this->getConfigData('active');
    }
}

Rate Model

Rate Calculation Model

namespace Vendor\Shipping\Model\Rate;

class RateCalculator
{
    private ConfigInterface $config;

    public function calculate(
        string $methodCode,
        \Magento\Shipping\Model\Request\Data $request
    ): float {
        $methodConfig = $this->config->getMethodConfig($methodCode);

        $baseRate = (float) $methodConfig['base_rate'];
        $rateType = $methodConfig['rate_type'];

        return match ($rateType) {
            'weight' => $this->calculateWeightRate($baseRate, $request),
            'price' => $this->calculatePriceRate($baseRate, $request),
            'flat' => $baseRate,
            'dimensional' => $this->calculateDimensionalRate($baseRate, $request),
        };
    }

    private function calculateWeightRate(
        float $baseRate,
        \Magento\Shipping\Model\Request\Data $request
    ): float {
        $weight = $request->getPackageWeight();
        return $baseRate * $weight;
    }

    private function calculatePriceRate(
        float $baseRate,
        \Magento\Shipping\Model\Request\Data $request
    ): float {
        $subtotal = $request->getPackageValue();
        return $subtotal * ($baseRate / 100);
    }

    private function calculateDimensionalRate(
        float $baseRate,
        \Magento\Shipping\Model\Request\Data $request
    ): float {
        $packages = $request->getPackages();
        $totalDimWeight = 0;

        foreach ($packages as $package) {
            $dimWeight = ($package['length'] * $package['width'] * $package['height']) / 139;
            $actualWeight = $package['weight'];
            $totalDimWeight += max($dimWeight, $actualWeight);
        }

        return $baseRate * $totalDimWeight;
    }
}

Rate Table

namespace Vendor\Shipping\Model\Rate;

class RateTable
{
    private array $table = [
        'US' => [
            'weight' => [
                ['min' => 0, 'max' => 1, 'rate' => 5.00],
                ['min' => 1, 'max' => 5, 'rate' => 10.00],
                ['min' => 5, 'max' => 999, 'rate' => 20.00],
            ],
        ],
        'CA' => [
            'weight' => [
                ['min' => 0, 'max' => 1, 'rate' => 10.00],
                ['min' => 1, 'max' => 5, 'rate' => 20.00],
                ['min' => 5, 'max' => 999, 'rate' => 35.00],
            ],
        ],
    ];

    public function getRate(string $country, string $type, float $value): ?float
    {
        foreach ($this->table[$country][$type] ?? [] as $tier) {
            if ($value >= $tier['min'] && $value < $tier['max']) {
                return $tier['rate'];
            }
        }
        return null;
    }
}

Tracking Implementation

Tracking Model

namespace Vendor\Shipping\Model\Carrier\Tracking;

class TrackingProvider implements \Magento\Shipping\Model\Carrier\TrackingInterface
{
    private TrackingGatewayInterface $gateway;
    private TrackingRepositoryInterface $trackingRepo;

    public function getTrackingInfo(
        \Magento\Shipping\Model\Tracking\Info $trackingInfo
    ): \Magento\Shipping\Model\Tracking\Result {
        $result = $this->trackingResultFactory->create();

        $trackingNumber = $trackingInfo->getTrackingNumber();
        $carrierCode = $trackingInfo->getCarrierCode();

        // Get tracking from gateway
        $trackingData = $this->gateway->track($carrierCode, $trackingNumber);

        $tracking = $this->trackFactory->create();
        $tracking->setTracking($trackingNumber);
        $tracking->setStatus($trackingData->getStatus());
        $tracking->setStatusDetail($trackingData->getDescription());
        $tracking->setCarrier($carrierCode);
        $tracking->setCarrierTitle($this->getCarrierTitle($carrierCode));

        // Add tracking history
        $history = [];
        foreach ($trackingData->getEvents() as $event) {
            $history[] = [
                'status' => $event->getStatus(),
                'location' => $event->getLocation(),
                'date' => $event->getDate(),
                'description' => $event->getDescription(),
            ];
        }

        $result->append($tracking);

        return $result;
    }
}

Tracking Gateway

namespace Vendor\Shipping\Gateway\Tracking;

interface TrackingGatewayInterface
{
    public function track(string $carrierCode, string $trackingNumber): TrackingData;
}

class HttpTrackingGateway implements TrackingGatewayInterface
{
    private HttpClientInterface $httpClient;
    private ConfigInterface $config;

    public function track(string $carrierCode, string $trackingNumber): TrackingData
    {
        $endpoint = $this->config->getTrackingEndpoint($carrierCode);

        $response = $this->httpClient->get($endpoint, [
            'query' => [
                'tracking' => $trackingNumber,
                'api_key' => $this->config->getApiKey($carrierCode),
            ],
        ]);

        return $this->parseResponse($response);
    }
}

Carrier Configuration

Admin Configuration

<!-- app/code/Vendor/Shipping/etc/system.xml -->
<config>
    <section id="carriers" translate="label" sortOrder="320">
        <group id="customcarrier" translate="label" type="text" sortOrder="100">
            <label>Custom Carrier</label>
            <field id="active" type="select" sortOrder="1">
                <label>Enabled</label>
                <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
            </field>
            <field id="title" type="text" sortOrder="10">
                <label>Title</label>
            </field>
            <field id="name" type="text" sortOrder="20">
                <label>Method Name</label>
            </field>
            <field id="apikey" type="obscure" sortOrder="30">
                <label>API Key</label>
                <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
            </field>
            <field id="sandbox_mode" type="select" sortOrder="40">
                <label>Sandbox Mode</label>
                <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
            </field>
            <field id="handling_type" type="select" sortOrder="50">
                <label>Handling Fee Type</label>
                <source_model>Magento\Shipping\Model\Config\Source\Handling\Type</source_model>
            </field>
            <field id="handling_fee" type="text" sortOrder="60">
                <label>Handling Fee</label>
            </field>
            <field id="sallowspecific" type="select" sortOrder="70">
                <label>Ship to Applicable Countries</label>
                <source_model>Magento\Shipping\Model\Config\Source\Allspecificcountries</source_model>
            </field>
            <field id="specificcountry" type="multiselect" sortOrder="80">
                <label>Ship to Specific Countries</label>
                <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
            </field>
            <field id="showmethod" type="select" sortOrder="90">
                <label>Show Method if Not Applicable</label>
                <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
            </field>
        </group>
    </section>
</config>

Carrier Registration

<!-- app/code/Vendor/Shipping/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Shipping\Model\CarrierPool">
        <arguments>
            <argument name="carriers" xsi:type="array">
                <item name="customcarrier" xsi:type="object">Vendor\Shipping\Model\Carrier\CustomCarrier</item>
            </argument>
        </arguments>
    </type>
</config>

config.xml Defaults

<!-- app/code/Vendor/Shipping/etc/config.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <carriers>
            <customcarrier>
                <active>0</active>
                <title>Custom Carrier</title>
                <name>Standard Shipping</name>
                <sandbox_mode>1</sandbox_mode>
                <handling_type>F</handling_type>
                <handling_fee>0</handling_fee>
                <sallowspecific>0</sallowspecific>
                <showmethod>0</showmethod>
            </customcarrier>
        </carriers>
    </default>
</config>

Quiz

1. What does _isFixed control?

Question 1 options

2. How do you register a custom carrier?

Question 2 options

3. What does config.xml provide?

Question 3 options

Flashcards

Question

How to register a carrier?

Answer

Add to di.xml CarrierPool arguments

Question

What is config.xml?

Answer

Provides default values for system.xml fields

Question

What does TrackingInterface provide?

Answer

Tracking information for shipped orders

Question

What is _isFixed?

Answer

Boolean indicating fixed vs variable pricing

Revision Notes

Key Takeaways

  • 1. Custom carriers extend AbstractCarrier and implement CarrierInterface
  • 2. Rate calculation supports weight, price, flat, and dimensional methods
  • 3. TrackingInterface provides shipment tracking capabilities
  • 4. Carriers register via di.xml CarrierPool argument
  • 5. config.xml provides default values for admin configuration

Interview Tips

  • Explain the complete carrier implementation lifecycle
  • Discuss different rate calculation methods
  • Describe how to add tracking to a carrier
  • Talk about carrier configuration hierarchy

Cheat Sheet

Custom Carrier:
  extends AbstractCarrier
  implements CarrierInterface
  collectRates() → Rate\Result
  isAvailable() → bool

Rate Types:
  weight → rate × weight
  price → rate × percentage
  flat → fixed amount
  dimensional → L×W×H / divisor

Registration:
  di.xml → CarrierPool arguments
  config.xml → default values
  system.xml → admin fields