Разработка кастомного сервиса 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 2.x сервис — это TypeScript-класс, зарегистрированный в IoC-контейнере и доступный через container.resolve(). Кастомные сервисы могут инкапсулировать бизнес-логику, интегрировать внешние API, оркестрировать несколько встроенных модулей и использоваться в Workflows, Subscribers, API-роутах.

Типы сервисов в Medusa 2.x

Тип Использование Регистрация
Module Service CRUD для модульных сущностей Module() декоратор
Custom Service Бизнес-логика, оркестрация src/modules/*/service.ts
Workflow Step Повторно-используемая step-логика createStep()
Provider Service Платёж, доставка, файлы Extends Abstract класс

Кастомный сервис в src/modules

// src/modules/loyalty/service.ts
import { MedusaContainer } from '@medusajs/framework/types';
import { Logger } from '@medusajs/framework/types';

type LoyaltyServiceDeps = {
  logger: Logger;
  // Встроенные модули доступны через container
};

export type LoyaltyPoint = {
  customerId: string;
  points: number;
  reason: string;
  orderId?: string;
};

export default class LoyaltyService {
  protected logger: Logger;

  constructor({ logger }: LoyaltyServiceDeps) {
    this.logger = logger;
  }

  async getCustomerPoints(customerId: string): Promise<number> {
    // Запрос к таблице loyalty_points
    const db = // получить connection через MikroORM
    const result = await db.query<{ total: number }>(
      `SELECT COALESCE(SUM(points), 0) as total
       FROM loyalty_points
       WHERE customer_id = $1 AND expires_at > NOW()`,
      [customerId]
    );
    return result[0]?.total ?? 0;
  }

  async addPoints(data: LoyaltyPoint): Promise<void> {
    this.logger.info(`Adding ${data.points} points to customer ${data.customerId}`);

    await db.query(
      `INSERT INTO loyalty_points (customer_id, points, reason, order_id, created_at, expires_at)
       VALUES ($1, $2, $3, $4, NOW(), NOW() + INTERVAL '1 year')`,
      [data.customerId, data.points, data.reason, data.orderId ?? null]
    );
  }

  async redeemPoints(customerId: string, pointsToRedeem: number): Promise<number> {
    const available = await this.getCustomerPoints(customerId);

    if (available < pointsToRedeem) {
      throw new Error(`Недостаточно баллов: доступно ${available}, запрошено ${pointsToRedeem}`);
    }

    await this.addPoints({
      customerId,
      points: -pointsToRedeem,
      reason: 'redemption',
    });

    // Конвертация баллов в скидку: 1 балл = 1 рубль
    return pointsToRedeem;
  }
}

Регистрация сервиса в IoC-контейнере

// src/modules/loyalty/index.ts
import { Module } from '@medusajs/framework/utils';
import LoyaltyService from './service';

export const LOYALTY_MODULE = 'loyaltyModuleService';

export default Module(LOYALTY_MODULE, {
  service: LoyaltyService,
});
// medusa-config.ts
export default defineConfig({
  modules: [
    { resolve: './src/modules/loyalty' },
  ],
});

Использование сервиса в Workflow

// src/workflows/steps/add-loyalty-points.ts
import { createStep, StepResponse } from '@medusajs/framework/workflows-sdk';
import { LOYALTY_MODULE } from '../../modules/loyalty';
import LoyaltyService from '../../modules/loyalty/service';

export const addLoyaltyPointsStep = createStep(
  'add-loyalty-points',
  async (input: { orderId: string; customerId: string; orderTotal: number }, context) => {
    const loyaltyService: LoyaltyService = context.container.resolve(LOYALTY_MODULE);

    // 1 балл за каждые 100 рублей заказа
    const pointsToAdd = Math.floor(input.orderTotal / 100);

    await loyaltyService.addPoints({
      customerId: input.customerId,
      points: pointsToAdd,
      reason: 'order_completed',
      orderId: input.orderId,
    });

    return new StepResponse(
      { pointsAdded: pointsToAdd },
      { customerId: input.customerId, pointsToAdd } // данные для rollback
    );
  },
  // Компенсация при ошибке в следующих шагах
  async ({ customerId, pointsToAdd }, context) => {
    const loyaltyService: LoyaltyService = context.container.resolve(LOYALTY_MODULE);
    await loyaltyService.addPoints({
      customerId,
      points: -pointsToAdd,
      reason: 'order_completed_rollback',
    });
  }
);

Использование в API-роуте

// src/api/store/loyalty/route.ts
import type { MedusaRequest, MedusaResponse } from '@medusajs/framework/http';
import { LOYALTY_MODULE } from '../../../modules/loyalty';
import LoyaltyService from '../../../modules/loyalty/service';

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const session = req.session as { customer_id?: string };

  if (!session.customer_id) {
    return res.status(401).json({ message: 'Не авторизован' });
  }

  const loyaltyService: LoyaltyService = req.scope.resolve(LOYALTY_MODULE);
  const points = await loyaltyService.getCustomerPoints(session.customer_id);

  res.json({ points, customer_id: session.customer_id });
};

Интеграция с внешним API в сервисе

// src/modules/erp-sync/service.ts
import { Logger } from '@medusajs/framework/types';
import axios, { AxiosInstance } from 'axios';

export default class ErpSyncService {
  private http: AxiosInstance;

  constructor({ logger }: { logger: Logger }) {
    this.http = axios.create({
      baseURL: process.env.ERP_API_URL,
      headers: { 'X-Api-Key': process.env.ERP_API_KEY },
      timeout: 10000,
    });

    // Retry логика
    this.http.interceptors.response.use(
      response => response,
      async error => {
        if (error.response?.status >= 500 && error.config._retryCount < 3) {
          error.config._retryCount = (error.config._retryCount || 0) + 1;
          await new Promise(r => setTimeout(r, 1000 * error.config._retryCount));
          return this.http.request(error.config);
        }
        return Promise.reject(error);
      }
    );
  }

  async syncInventory(variantSku: string): Promise<number> {
    const { data } = await this.http.get(`/inventory/${variantSku}`);
    return data.quantity;
  }

  async pushOrderToErp(order: Record<string, unknown>): Promise<string> {
    const { data } = await this.http.post('/orders', {
      external_id: order.id,
      items: order.items,
      total: order.total,
      customer_email: order.email,
    });
    return data.erp_order_id;
  }
}

Тестирование сервисов

// src/modules/loyalty/__tests__/service.test.ts
import LoyaltyService from '../service';

const mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn() };
const mockDb = { query: jest.fn() };

describe('LoyaltyService', () => {
  let service: LoyaltyService;

  beforeEach(() => {
    service = new LoyaltyService({ logger: mockLogger as any });
    (service as any).db = mockDb;
  });

  it('should calculate points correctly', async () => {
    mockDb.query.mockResolvedValueOnce([{ total: 150 }]);
    const points = await service.getCustomerPoints('cust_123');
    expect(points).toBe(150);
  });

  it('should throw when redeeming more points than available', async () => {
    mockDb.query.mockResolvedValueOnce([{ total: 50 }]);
    await expect(service.redeemPoints('cust_123', 100))
      .rejects.toThrow('Недостаточно баллов');
  });
});

Сроки разработки

  • Простой сервис (получение/запись данных, 1–2 операции): 1–2 дня
  • Сервис с интеграцией внешнего API, retry логикой, тестами: 3–5 дней
  • Сложный сервис (loyalty-программа, B2B pricing, кастомный инвентарь): 1–3 недели