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

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Інтеграція служб доставки в Medusa.js
Середня
~3-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

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

Medusa.js — це фреймворк без веб-серверу для комерції на 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';

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

    // Валідація параметра при створенні 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;

    // Знаходимо виконання за номером відстеження
    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 }) {
        // Відправлення електронної пошти через Notification Provider
        // або оновлення метаданих замовлення
        console.log(`Виконання ${fulfillment_id}: ${tracking_number} -> ${status}`);
    }
}

export default TrackingUpdatedSubscriber;

Користувацька ShippingOption в Admin

Після реєстрації провайдера в адміні створюється ShippingOption через інтерфейс або 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 днів.