Module Setup and Carrier Configuration
Module Structure
app/code/Vendor/FastShip/
├── registration.php
├── etc/
│ ├── module.xml
│ ├── di.xml
│ ├── system.xml
│ └── carrier.xml
├── Model/
│ ├── Carrier.php
│ ├── Rate\Result.php
│ ├── Rate\Result\Method.php
│ └── Source\Method.php
├── Carrier/
│ └── FastShipCarrier.php
├── Observer/
│ └── AddTrackingInfo.php
└── view/
└── frontend/
└── web/
└── js/
└── view.js
registration.php and module.xml
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Vendor_FastShip',
__DIR__
);
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_FastShip" setup_version="1.0.0">
<sequence>
<module name="Magento_Shipping"/>
<module name="Magento_Quote"/>
</sequence>
</module>
</config>
System Configuration (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="carriers" translate="label" type="text" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1">
<group id="fastship" translate="label" type="text" sortOrder="330" showInDefault="1" showInWebsite="1" showInStore="1">
<label>FastShip Shipping</label>
<field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="name" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Method Name</label>
</field>
<field id="title" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Title</label>
</field>
<field id="sallowspecific" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Ship to Applicable Countries</label>
<source_model>Magento\Shipping\Model\Config\Source\Allspecificcountries</source_model>
</field>
<field id="specificcountry" translate="label" type="multiselect" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Ship to Specific Countries</label>
<source_model>Magento\Directory\Model\Config\Source\Country</source_model>
</field>
<field id="handling_fee" translate="label" type="text" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Handling Fee</label>
<validate>validate-number validate-zero-or-greater</validate>
</field>
<field id="free_shipping_enable" translate="label" type="select" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Free Shipping Enable</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="free_shipping_amount" translate="label" type="text" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Free Shipping Amount</label>
<validate>validate-number validate-zero-or-greater</validate>
</field>
<field id="specificerrmsg" translate="label" type="textarea" sortOrder="9" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Displayed Error Message</label>
</field>
</group>
</section>
</system>
</config>
di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Shipping\Model\CarrierFactory">
<arguments>
<argument name="configProviders" xsi:type="array">
<item name="fastship" xsi:type="object">Vendor\FastShip\Model\Carrier\FastShipCarrier</item>
</argument>
</arguments>
</type>
</config>
Carrier Implementation and Rate Calculation
Carrier Class
<?php
namespace Vendor\FastShip\Model\Carrier;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Rate\Result;
use Magento\Store\Model\Store;
class FastShipCarrier extends AbstractCarrier implements CarrierInterface
{
protected $_code = 'fastship';
protected $_isFixed = false;
protected $_renderedRate = false;
private array $methods = [];
public function __construct(
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
\Psr\Log\LoggerInterface $logger,
array $data = [],
private \Vendor\FastShip\Model\Source\Method $methodSource,
) {
parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
}
public function collectRates(RateRequest $request): Result
{
if (!$this->getConfigFlag('active')) {
return false;
}
$result = $this->_rateResultFactory->create();
// Check free shipping
$freeShippingAmount = (float) $this->getConfigData('free_shipping_amount');
$freeShippingEnable = $this->getConfigFlag('free_shipping_enable');
if ($freeShippingEnable && $request->getPackageValue() >= $freeShippingAmount) {
$method = $this->_rateResultMethodFactory->create();
$method->setCarrier('fastship');
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod('free');
$method->setMethodTitle('Free Shipping');
$method->setPrice(0);
$method->setCost(0);
$result->append($method);
return $result;
}
// Calculate rates for each method
foreach ($this->getAvailableMethods() as $methodCode => $methodConfig) {
$rate = $this->calculateRate($request, $methodCode, $methodConfig);
if ($rate !== false) {
$result->append($rate);
}
}
return $result;
}
private function calculateRate(RateRequest $request, string $methodCode, array $config): ?\Magento\Shipping\Model\Rate\Method
{
$weight = $request->getPackageWeight();
$destCountry = $request->getDestCountryId();
$destZip = $request->getDestPostcode();
// Rate calculation rules
$baseRate = (float) $config['base_rate'];
$perKgRate = (float) $config['per_kg_rate'];
$handlingFee = (float) $this->getConfigData('handling_fee');
// Calculate cost
$cost = $baseRate + ($weight * $perKgRate) + $handlingFee;
// Apply country-specific rules
if ($destCountry === 'US') {
$cost *= 1.0; // Domestic rate
} else {
$cost *= 2.5; // International rate
}
// Apply distance-based surcharge (simplified)
$surcharge = $this->calculateDistanceSurcharge($destZip);
$cost += $surcharge;
$method = $this->_rateResultMethodFactory->create();
$method->setCarrier('fastship');
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod($methodCode);
$method->setMethodTitle($config['title']);
$method->setPrice($cost);
$method->setCost($cost);
return $method;
}
private function calculateDistanceSurcharge(string $destZip): float
{
// Simplified distance calculation
// In production, use a geocoding service
$originZip = '10001'; // NYC origin
$distance = abs((int)$destZip - (int)$originZip) / 100;
return $distance * 0.5; // $0.50 per distance unit
}
private function getAvailableMethods(): array
{
return [
'standard' => ['title' => 'Standard (5-7 days)', 'base_rate' => 5.99, 'per_kg_rate' => 1.50],
'express' => ['title' => 'Express (2-3 days)', 'base_rate' => 12.99, 'per_kg_rate' => 2.50],
'overnight' => ['title' => 'Overnight', 'base_rate' => 24.99, 'per_kg_rate' => 4.00],
];
}
public function getAllowedMethods(): array
{
return ['standard' => 'Standard', 'express' => 'Express', 'overnight' => 'Overnight'];
}
public function isTrackingAvailable(): bool
{
return true;
}
}
Rate Result Classes
<?php
namespace Vendor\FastShip\Model\Rate;
class Result extends \Magento\Shipping\Model\Rate\Result
{
public function __construct(
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
\Magento\Store\Model\StoreManagerInterface $storeManager,
) {
parent::__construct($priceCurrency, $storeManager);
}
}
<?php
namespace Vendor\FastShip\Model\Rate;
class Method extends \Magento\Shipping\Model\Rate\Method
{
private string $trackingUrl = '';
public function getTrackingUrl(): string
{
return $this->trackingUrl;
}
public function setTrackingUrl(string $url): self
{
$this->trackingUrl = $url;
return $this;
}
}
Tracking Integration and Observers
Tracking Data Provider
<?php
namespace Vendor\FastShip\Model\Tracking;
use Magento\Shipping\Model\Tracking\Result\Item;
class TrackingProvider implements \Magento\Shipping\Model\Tracking\ResultInterface
{
private array $trackings = [];
public function __construct(
private \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
) {
}
public function fetchTrackinfo(string $trackingNumber): array
{
// API call to FastShip tracking service
$trackingData = $this->callTrackingApi($trackingNumber);
$track = [];
if (!empty($trackingData)) {
$trackItem = new Item();
$trackItem->setData([
'carrier' => 'fastship',
'carrier_title' => 'FastShip Shipping',
'tracking_number' => $trackingNumber,
'status' => $trackingData['status'],
'status_detail' => $trackingData['detail'],
'estimated_delivery' => $trackingData['estimated_delivery'],
'trackings' => $trackingData['history'] ?? [],
]);
$this->trackings[] = $trackItem;
}
return $this->trackings;
}
private function callTrackingApi(string $trackingNumber): array
{
$apiUrl = $this->scopeConfig->getValue('carriers/fastship/tracking_api_url');
$apiKey = $this->scopeConfig->getValue('carriers/fastship/tracking_api_key');
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $apiUrl . '/' . $trackingNumber,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
return json_decode($response, true);
}
return [];
}
}
Observer for Shipment Creation
<?php
namespace Vendor\FastShip\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magento\Sales\Model\Order\Shipment;
class AddTrackingInfo implements ObserverInterface
{
public function __construct(
private \Psr\Log\LoggerInterface $logger,
) {
}
public function execute(Observer $observer): void
{
/** @var Shipment $shipment */
$shipment = $observer->getEvent()->getShipment();
if ($shipment->getOrder()->getShippingMethod() !== 'fastship') {
return;
}
$tracks = $shipment->getTracks();
foreach ($tracks as $track) {
if ($track->getCarrierCode() === 'fastship') {
$trackingNumber = $track->getTrackNumber();
// Send tracking info to FastShip API
$this->registerTrackingNumber($trackingNumber, $shipment);
// Set estimated delivery
$estimatedDelivery = $this->calculateEstimatedDelivery();
$track->setTitle('FastShip - Estimated: ' . $estimatedDelivery);
}
}
}
private function registerTrackingNumber(string $trackingNumber, Shipment $shipment): void
{
$this->logger->info('FastShip: Registered tracking ' . $trackingNumber);
// API call to register tracking with FastShip
}
private function calculateEstimatedDelivery(): string
{
$days = 5; // Default standard
$shippingMethod = $this->getShippingMethod();
return match($shippingMethod) {
'overnight' => date('Y-m-d', strtotime('+1 day')),
'express' => date('Y-m-d', strtotime('+3 days')),
default => date('Y-m-d', strtotime('+7 days')),
};
}
}
Observer Registration
<!-- etc/events.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="sales_order_shipment_save_after">
<observer name="fastship_add_tracking" instance="Vendor\FastShip\Observer\AddTrackingInfo"/>
</event>
</config>
Testing and Configuration
Carrier Test
<?php
namespace Vendor\FastShip\Test\Unit\Model\Carrier;
use PHPUnit\Framework\TestCase;
use Vendor\FastShip\Model\Carrier\FastShipCarrier;
class FastShipCarrierTest extends TestCase
{
public function testCollectRatesReturnsResult(): void
{
$scopeConfig = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class);
$scopeConfig->method('isSetFlag')->with('carriers/fastship/active')->willReturn(true);
$scopeConfig->method('getValue')->willReturnMap([
['carriers/fastship/handling_fee', null, null, '0'],
['carriers/fastship/free_shipping_amount', null, null, '100'],
]);
$rateErrorFactory = $this->createMock(
\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory::class
);
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
$resultFactory = $this->createMock(\Magento\Shipping\Model\Rate\ResultFactory::class);
$methodFactory = $this->createMock(\Magento\Quote\Model\Quote\Address\RateResult\MethodFactory::class);
$carrier = new FastShipCarrier(
$scopeConfig,
$rateErrorFactory,
$logger,
[],
$this->createMock(\Vendor\FastShip\Model\Source\Method::class)
);
$request = $this->createMock(\Magento\Quote\Model\Quote\Address\RateRequest::class);
$request->method('getPackageValue')->willReturn(50);
// Test carrier returns rates
$result = $carrier->collectRates($request);
$this->assertNotNull($result);
}
public function testGetAllowedMethods(): void
{
$carrier = $this->createCarrier();
$methods = $carrier->getAllowedMethods();
$this->assertArrayHasKey('standard', $methods);
$this->assertArrayHasKey('express', $methods);
$this->assertArrayHasKey('overnight', $methods);
}
}
Complete Configuration Example
<!-- etc/config.xml - Default values -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<carriers>
<fastship>
<active>1</active>
<name>FastShip</name>
<title>FastShip Shipping</title>
<sallowspecific>0</sallowspecific>
<handling_fee>2.50</handling_fee>
<free_shipping_enable>1</free_shipping_enable>
<free_shipping_amount>75</free_shipping_amount>
<specificerrmsg>This shipping method is currently unavailable.</specificerrmsg>
<model>Vendor\FastShip\Model\Carrier\FastShipCarrier</model>
</fastship>
</carriers>
</default>
</config>
Quiz
1. What must a custom carrier class extend?
2. What method calculates shipping rates?
3. Where is carrier configuration defined?
Flashcards
Question
What does a carrier extend?
Click to reveal answer
Answer
Magento\Shipping\Model\Carrier\AbstractCarrier
Question
What is the rate calculation method?
Click to reveal answer
Answer
collectRates(RateRequest $request): Result
Question
How do you add tracking?
Click to reveal answer
Answer
Implement TrackingProviderInterface and register in di.xml
Question
Where is carrier config?
Click to reveal answer
Answer
etc/system.xml under <section id="carriers">
Question
How do you enable free shipping?
Click to reveal answer
Answer
Set free_shipping_enable and free_shipping_amount in config
Revision Notes
Key Takeaways
- 1. Custom carriers extend AbstractCarrier and implement CarrierInterface
- 2. collectRates() calculates rates based on package weight, destination, and rules
- 3. system.xml defines configuration fields for admin
- 4. Tracking integration uses observer or dedicated provider class
- 5. Rate calculation can include base rate, per-kg rate, handling fee, and surcharges
Interview Tips
- • Explain the rate calculation flow in Magento shipping
- • Describe how to add country-specific shipping rules
- • Discuss tracking integration patterns
- • Talk about free shipping configuration and conditions
Cheat Sheet
Custom Shipping Carrier:
Extends AbstractCarrier implements CarrierInterface
collectRates(RateRequest) → Result
getAllowedMethods() → array
Rate Calculation:
base_rate + (weight × per_kg_rate) + handling_fee
Country multiplier for international
Distance surcharge for zones
Configuration:
etc/system.xml → Admin config fields
etc/di.xml → Register carrier
etc/config.xml → Default values
Tracking:
TrackingProvider → fetchTrackinfo()
Observer on shipment save
Register tracking with external API