Интеграция служб доставки в Medusa.js

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Интеграция служб доставки в Medusa.js
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Интеграция служб доставки в Medusa.js

Medusa.js — headless commerce-фреймворк на Node.js — строится вокруг концепции плагинов. Доставка реализуется через FulfillmentProvider: классы, которые инкапсулируют работу с конкретным перевозчиком. Стандартный manual провайдер только создаёт записи в базе — без расчёта тарифов и без отправки данных перевозчику.

Архитектура Fulfillment Provider

Каждый провайдер — это сервис, зарегистрированный в контейнере Medusa. Базовый интерфейс:

import { AbstractFulfillmentService } from '@medusajs/medusa';
import { Cart, Fulfillment, LineItem, Order } from '@medusajs/medusa/dist/models';

class MyCourierFulfillmentService extends AbstractFulfillmentService {
    static identifier = 'my-courier';

    // Доступные методы доставки (отображаются в shipping options)
    async getFulfillmentOptions(): Promise<Record<string, unknown>[]> {
        return [
            { id: 'my-courier-standard', name: 'Стандарт' },
            { id: 'my-courier-express', name: 'Экспресс' },
        ];
    }

    // Валидация option при создании ShippingOption в админке
    async validateOption(data: Record<string, unknown>): Promise<boolean> {
        return ['my-courier-standard', 'my-courier-express'].includes(
            data.id as string
        );
    }

    // Расчёт стоимости для конкретной корзины
    async calculatePrice(
        optionData: Record<string, unknown>,
        data: Record<string, unknown>,
        cart: Cart
    ): Promise<number> {
        const weight = cart.items.reduce(
            (sum, item) => sum + (item.variant?.weight ?? 100) * item.quantity,
            0
        );
        const toCity = cart.shipping_address?.city ?? '';
        const price = await this.getApiRate(optionData.id as string, weight, toCity);
        return price; // в наименьших единицах валюты (копейки/центы)
    }

    // Создание отправления
    async createFulfillment(
        data: Record<string, unknown>,
        items: LineItem[],
        order: Order,
        fulfillment: Fulfillment
    ): Promise<Record<string, unknown>> {
        const shipment = await this.apiClient.createShipment({
            service:    data.id,
            recipient:  order.shipping_address,
            items:      items.map(i => ({ sku: i.variant?.sku, qty: i.quantity })),
            order_ref:  order.display_id.toString(),
        });
        return { tracking_number: shipment.tracking, shipment_id: shipment.id };
    }

    // Отмена отправления
    async cancelFulfillment(fulfillment: Fulfillment): Promise<Record<string, unknown>> {
        await this.apiClient.cancelShipment(fulfillment.data.shipment_id as string);
        return {};
    }

    // Нужен ли возврат — провайдер решает
    async canCalculate(data: Record<string, unknown>): Promise<boolean> {
        return true;
    }

    async validateFulfillmentData(
        optionData: Record<string, unknown>,
        data: Record<string, unknown>,
        cart: Cart
    ): Promise<Record<string, unknown>> {
        return data;
    }
}

export default MyCourierFulfillmentService;

Регистрация в проекте Medusa

В medusa-config.js плагин подключается через массив plugins, если оформлен как npm-пакет. Для локальной разработки достаточно зарегистрировать сервис напрямую:

// src/services/my-courier-fulfillment.ts — тот же класс выше

// src/loaders/fulfillment.ts
import { asClass } from 'awilix';

export default async (container) => {
    container.register({
        myСourierFulfillmentService: asClass(MyCourierFulfillmentService).singleton(),
    });
};

В Medusa v2 (с модульной архитектурой) провайдер декларируется через defineProvider:

// В модуле доставки
import { ModuleProvider, Modules } from '@medusajs/utils';

export default ModuleProvider(Modules.FULFILLMENT, {
    services: [MyCourierFulfillmentService],
});

HTTP-клиент для API перевозчика

// src/services/my-courier-api.ts
import axios, { AxiosInstance } from 'axios';

export class MyCourierApiClient {
    private client: AxiosInstance;

    constructor(apiKey: string) {
        this.client = axios.create({
            baseURL: 'https://api.mycourier.ru/v2',
            timeout: 10_000,
            headers: { Authorization: `Bearer ${apiKey}` },
        });
    }

    async getRates(serviceCode: string, weight: number, toCity: string): Promise<number> {
        const { data } = await this.client.post('/calculate', {
            service: serviceCode,
            weight:  Math.max(0.1, weight / 1000), // граммы -> кг
            to_city: toCity,
        });
        // Возвращаем в копейках для Medusa
        return Math.round(data.price * 100);
    }

    async createShipment(payload: object): Promise<{ tracking: string; id: string }> {
        const { data } = await this.client.post('/shipments', payload);
        return data;
    }

    async cancelShipment(shipmentId: string): Promise<void> {
        await this.client.delete(`/shipments/${shipmentId}`);
    }
}

Webhook: обновление статуса заказа

Medusa поддерживает события через EventBus. Webhook от перевозчика попадает в кастомный роут:

// src/api/routes/webhooks/courier.ts
import { Router } from 'express';
import type { MedusaRequest, MedusaResponse } from '@medusajs/medusa';

const router = Router();

router.post('/courier/webhook', async (req: MedusaRequest, res: MedusaResponse) => {
    const { tracking_number, status, event } = req.body;

    // Находим fulfillment по трекинг-номеру
    const fulfillmentRepo = req.scope.resolve('fulfillmentRepository');
    const fulfillment = await fulfillmentRepo.findOne({
        where: { data: { tracking_number } },
    });

    if (!fulfillment) {
        return res.sendStatus(404);
    }

    const orderService = req.scope.resolve('orderService');

    if (event === 'delivered') {
        await orderService.capturePayment(fulfillment.order_id);
        // или просто обновляем мета
    }

    const eventBus = req.scope.resolve('eventBusService');
    await eventBus.emit('fulfillment.tracking_updated', {
        fulfillment_id:  fulfillment.id,
        tracking_number,
        status,
    });

    res.sendStatus(200);
});

export default router;

Роут регистрируется в src/api/index.ts:

import courierWebhookRouter from './routes/webhooks/courier';

export default (rootDirectory: string) => {
    const router = Router();
    router.use('/store', courierWebhookRouter);
    return router;
};

Subscriber: уведомление покупателя

// src/subscribers/tracking-updated.ts
import { OrderService } from '@medusajs/medusa';

class TrackingUpdatedSubscriber {
    private orderService: OrderService;

    constructor({ orderService, eventBusService }) {
        this.orderService = orderService;
        eventBusService.subscribe(
            'fulfillment.tracking_updated',
            this.handleTrackingUpdated.bind(this)
        );
    }

    async handleTrackingUpdated({ fulfillment_id, tracking_number, status }) {
        // Отправка email через Notification Provider
        // или обновление метаданных заказа
        console.log(`Fulfillment ${fulfillment_id}: ${tracking_number} -> ${status}`);
    }
}

export default TrackingUpdatedSubscriber;

Кастомный ShippingOption в Admin

После регистрации провайдера в админке создаётся Shipping Option через UI или API:

curl -X POST http://localhost:9000/admin/shipping-options \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "MyCourier Стандарт",
    "region_id": "reg_01HXXX",
    "provider_id": "my-courier",
    "data": { "id": "my-courier-standard" },
    "price_type": "calculated",
    "requirements": [
      { "type": "max_subtotal", "amount": 100000 }
    ]
  }'

price_type: "calculated" означает, что цена определяется через calculatePrice() провайдера, а не фиксирована.

Сроки реализации

Базовый провайдер с расчётом тарифов и созданием отправлений: 2–3 дня. Добавление webhook-обработчика, subscriber уведомлений и обновления статусов: плюс 1–2 дня. Полноценный npm-пакет с конфигурацией, тестами и поддержкой Medusa v1 и v2: 5–7 дней.