Розробка кастомного плагіна доставки Magento 2

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка кастомного плагіна доставки Magento 2
Складна
~5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка користувацького плагіна доставки Magento 2

Magento 2 має розвинену систему доставки — Shipping Carriers, Rates, Carriers Facade. Але вбудовані методи (UPS, FedEx, DHL) налаштовані на західний ринок. Для роботи з місцевими перевізниками, нестандартною логікою тарифікації або інтеграцією з внутрішньою WMS потрібен користувацький carrier-модуль.

Архітектура Shipping Carrier у Magento 2

Модуль будується за стандартною структурою Magento 2:

app/code/Vendor/MyCourier/
├── etc/
│   ├── module.xml
│   ├── config.xml          # значення конфігу за замовчуванням
│   ├── adminhtml/
│   │   └── system.xml      # форма налаштувань у Admin > Stores > Config
│   └── frontend/
│       └── routes.xml      # якщо потрібен frontend-контролер
├── Model/
│   └── Carrier/
│       └── MyCourier.php   # основний клас
├── registration.php
└── composer.json

Центральний клас розширює \Magento\Shipping\Model\Carrier\AbstractCarrier:

<?php
// Model/Carrier/MyCourier.php

namespace Vendor\MyCourier\Model\Carrier;

use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Rate\Result;

class MyCourier extends AbstractCarrier implements CarrierInterface {

    protected $_code = 'mycourier';

    public function collectRates( RateRequest $request ): ?Result {
        if ( ! $this->getConfigFlag( 'active' ) ) {
            return null;
        }

        /** @var Result $result */
        $result = $this->_rateResultFactory->create();

        $rates = $this->fetchRatesFromApi( $request );

        foreach ( $rates as $rateData ) {
            /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
            $method = $this->_rateMethodFactory->create();
            $method->setCarrier( $this->_code );
            $method->setCarrierTitle( $this->getConfigData( 'title' ) );
            $method->setMethod( $rateData['code'] );
            $method->setMethodTitle( $rateData['name'] );
            $method->setPrice( $rateData['price'] );
            $method->setCost( $rateData['price'] );
            $result->append( $method );
        }

        return $result;
    }

    public function getAllowedMethods(): array {
        return [ $this->_code => $this->getConfigData( 'title' ) ];
    }
}

Розрахунок тарифів: запит до API

Для роботи з зовнішніми API використовуйте \Magento\Framework\HTTP\Client\Curl — стандартний HTTP-клієнт Magento 2, не потребує додаткових залежностей:

private function fetchRatesFromApi( RateRequest $request ): array {
    $apiKey   = $this->getConfigData( 'api_key' );
    $fromCity = $this->getConfigData( 'from_city' );
    $toCity   = $request->getDestCity();
    $postcode = $request->getDestPostcode();

    $weight = 0;
    foreach ( $request->getAllItems() as $item ) {
        if ( $item->getParentItem() ) {
            continue; // пропускаємо configurable parent
        }
        $weight += $item->getWeight() * $item->getQty();
    }

    $payload = json_encode([
        'from'     => $fromCity,
        'to_city'  => $toCity,
        'postcode' => $postcode,
        'weight'   => max( 0.1, $weight ),
        'currency' => $request->getPackageCurrency()->getCurrencyCode(),
    ]);

    $this->_curl->addHeader( 'Authorization', 'Bearer ' . $apiKey );
    $this->_curl->addHeader( 'Content-Type', 'application/json' );
    $this->_curl->setTimeout( 10 );

    try {
        $this->_curl->post( 'https://api.mycourier.ru/v2/rates', $payload );
        $body   = $this->_curl->getBody();
        $status = $this->_curl->getStatus();
    } catch ( \Exception $e ) {
        $this->_logger->error( 'MyCourier API error: ' . $e->getMessage() );
        return [];
    }

    if ( $status !== 200 ) {
        return [];
    }

    $data = json_decode( $body, true );
    return $data['services'] ?? [];
}

DI в конструкторі:

public function __construct(
    \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
    \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
    \Psr\Log\LoggerInterface $logger,
    \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory,
    \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory,
    \Magento\Framework\HTTP\Client\Curl $curl,
    array $data = []
) {
    $this->_curl = $curl;
    parent::__construct( $scopeConfig, $rateErrorFactory, $logger, $rateResultFactory, $rateMethodFactory, $data );
}

Конфігурація модуля

etc/config.xml встановлює стандартні значення — без цього файлу поля конфіга повертають null:

<?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>
            <mycourier>
                <active>0</active>
                <title>MyCourier</title>
                <from_city>Москва</from_city>
                <sallowspecific>0</sallowspecific>
                <sort_order>10</sort_order>
            </mycourier>
        </carriers>
    </default>
</config>

etc/adminhtml/system.xml додає секцію до Stores > Configuration > Sales > Shipping Methods:

<section id="carriers">
    <group id="mycourier" translate="label" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
        <label>MyCourier</label>
        <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1">
            <label>Увімкнено</label>
            <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
        </field>
        <field id="api_key" translate="label" type="text" sortOrder="20" showInDefault="1">
            <label>API Key</label>
            <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
        </field>
        <field id="from_city" translate="label" type="text" sortOrder="30" showInDefault="1">
            <label>Місто відправлення</label>
        </field>
    </group>
</section>

Використовуйте backend Encrypted для API-ключа — він шифрується в базі даних через \Magento\Framework\Encryption\EncryptorInterface.

Observer: створення відправлення після оплати

<?php
// Observer/CreateShipment.php

namespace Vendor\MyCourier\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order\ShipmentFactory;

class CreateShipment implements ObserverInterface {

    public function execute( Observer $observer ): void {
        /** @var \Magento\Sales\Model\Order $order */
        $order = $observer->getEvent()->getOrder();

        if ( ! $order->canShip() ) {
            return;
        }
        if ( strpos( $order->getShippingMethod(), 'mycourier' ) === false ) {
            return;
        }

        $trackingNumber = $this->courierApi->createShipment( $order );
        if ( ! $trackingNumber ) {
            return;
        }

        // Створюємо shipment у Magento
        $shipment = $this->shipmentFactory->create(
            $order,
            $this->prepareItems( $order ),
            [ [
                'carrier_code'  => 'mycourier',
                'title'         => 'MyCourier',
                'number'        => $trackingNumber,
            ] ]
        );
        $shipment->register();
        $shipment->getOrder()->setIsInProcess( true );

        $this->transaction
            ->addObject( $shipment )
            ->addObject( $shipment->getOrder() )
            ->save();
    }
}

Реєструємо observer у etc/events.xml:

<config>
    <event name="sales_order_invoice_pay">
        <observer name="mycourier_create_shipment"
                  instance="Vendor\MyCourier\Observer\CreateShipment"/>
    </event>
</config>

Plugin для чекауту: ПВЗ

Magento 2 дозволяє вбудувати користувацький UI-компонент у checkout через checkout_index_index.xml. Додайте поле вибору ПВЗ після вибору методу доставки:

<!-- view/frontend/layout/checkout_index_index.xml -->
<referenceBlock name="checkout.root">
    <arguments>
        <argument name="jsLayout" xsi:type="array">
            <item name="components" xsi:type="array">
                <item name="checkout" xsi:type="array">
                    <item name="children" xsi:type="array">
                        <item name="steps" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="shipping-step" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="shippingAddress" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="mycourier-pvz" xsi:type="array">
                                                    <item name="component" xsi:type="string">
                                                        Vendor_MyCourier/js/pvz-selector
                                                    </item>
                                                    <item name="sortOrder" xsi:type="string">200</item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </item>
            </item>
        </argument>
    </arguments>
</referenceBlock>

Кешування через Magento Cache

Для розрахунку тарифів важливо використовувати кеш Magento, а не статичні змінні:

use Magento\Framework\App\CacheInterface;
use Magento\Framework\Serialize\SerializerInterface;

private function getCachedRates( string $cacheKey ): ?array {
    $cached = $this->cache->load( $cacheKey );
    if ( $cached ) {
        return $this->serializer->unserialize( $cached );
    }
    return null;
}

private function saveCachedRates( string $cacheKey, array $rates ): void {
    $this->cache->save(
        $this->serializer->serialize( $rates ),
        $cacheKey,
        [ 'mycourier_rates' ],
        1800
    );
}

Тег mycourier_rates дозволяє інвалідувати весь кеш тарифів командою:

bin/magento cache:clean mycourier_rates

Встановлення та розгортання

# Копіюємо модуль
cp -r Vendor/MyCourier app/code/Vendor/MyCourier

# Включаємо
bin/magento module:enable Vendor_MyCourier
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy ru_RU en_US -f

# Для production
bin/magento deploy:mode:set production

Важливо: після будь-якої зміни DI (конструкторів, плагінів, observers) потребує setup:di:compile. Без нього зміни не підтягуватимуться через кешування генерації коду.

Тестування

Unit-тест для collectRates:

public function testCollectsRatesWhenApiReturnsData(): void {
    $this->curlMock->method( 'getStatus' )->willReturn( 200 );
    $this->curlMock->method( 'getBody' )->willReturn( json_encode([
        'services' => [
            [ 'code' => 'standard', 'name' => 'Стандарт', 'price' => 350.0 ],
        ]
    ]));

    $result = $this->carrier->collectRates( $this->createRateRequest() );

    $this->assertInstanceOf( Result::class, $result );
    $rates = $result->getAllRates();
    $this->assertCount( 1, $rates );
    $this->assertEquals( 350.0, $rates[0]->getPrice() );
}

Строки реалізації

Базовий carrier з розрахунком тарифів через API та конфігурацією в адміністрації: 3–4 дня. Додавання observer для створення відправлень, трекінгу та повідомлень: плюс 2–3 дня. UI-компонент вибору ПВЗ у checkout з користувацькими полями адреси: плюс 2–3 дня. Інтеграція з Magento MSI (multi-source inventory) для обліку залишків по складах: плюс 3–5 днів.