Интеграция платёжных шлюзов в 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 — open-source headless e-commerce платформа на Node.js. Платёжные провайдеры реализуются как плагины через стандартный AbstractPaymentProvider интерфейс. Официальные интеграции есть для Stripe, PayPal, Klarna. Кастомный провайдер пишется за 2–4 рабочих дня.

Архитектура провайдера

Каждый провайдер реализует набор методов, которые Medusa вызывает в определённых точках checkout flow:

  • initiatePayment — создать сессию/ссылку на оплату
  • authorizePayment — подтвердить авторизацию (после редиректа)
  • capturePayment — списать средства
  • refundPayment — выполнить возврат
  • cancelPayment — отменить платёж
  • retrievePayment — получить текущий статус
  • getPaymentStatus — маппинг статуса провайдера в Medusa-статус

Базовый провайдер

import {
    AbstractPaymentProvider,
    PaymentProviderError,
    PaymentProviderSessionResponse,
    PaymentSessionStatus,
    CreatePaymentProviderSession,
    UpdatePaymentProviderSession,
} from '@medusajs/framework/utils';

class MyPayProvider extends AbstractPaymentProvider<MyPayOptions> {
    static identifier = 'mypay';

    private client: MyPayClient;

    constructor(container: unknown, options: MyPayOptions) {
        super(container, options);
        this.client = new MyPayClient(options.apiKey, options.secretKey);
    }

    async initiatePayment(
        data: CreatePaymentProviderSession
    ): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
        const { amount, currency_code, context } = data;

        try {
            const payment = await this.client.createPayment({
                amount:     Math.round(amount),
                currency:   currency_code.toUpperCase(),
                order_id:   context.cart_id,
                email:      context.customer?.email,
                callback_url: `${process.env.BACKEND_URL}/mypay/webhook`,
            });

            return {
                id:   payment.id,
                data: {
                    payment_id:  payment.id,
                    payment_url: payment.checkout_url,
                    status:      payment.status,
                },
            };
        } catch (e) {
            return { error: e.message, code: 'initiate_failed', detail: e };
        }
    }

    async authorizePayment(
        paymentSessionData: Record<string, unknown>
    ): Promise<PaymentProviderError | { status: PaymentSessionStatus; data: Record<string, unknown> }> {
        const status = await this.getPaymentStatus(paymentSessionData);
        return { status, data: paymentSessionData };
    }

    async getPaymentStatus(
        paymentSessionData: Record<string, unknown>
    ): Promise<PaymentSessionStatus> {
        const payment = await this.client.getPayment(paymentSessionData.payment_id as string);

        const statusMap: Record<string, PaymentSessionStatus> = {
            pending:   PaymentSessionStatus.PENDING,
            succeeded: PaymentSessionStatus.AUTHORIZED,
            failed:    PaymentSessionStatus.ERROR,
            cancelled: PaymentSessionStatus.CANCELED,
        };

        return statusMap[payment.status] ?? PaymentSessionStatus.PENDING;
    }

    async capturePayment(
        paymentData: Record<string, unknown>
    ): Promise<PaymentProviderError | Record<string, unknown>> {
        try {
            await this.client.capture(paymentData.payment_id as string);
            return { ...paymentData, status: 'captured' };
        } catch (e) {
            return { error: e.message, code: 'capture_failed', detail: e };
        }
    }

    async refundPayment(
        paymentData: Record<string, unknown>,
        refundAmount: number
    ): Promise<PaymentProviderError | Record<string, unknown>> {
        try {
            const refund = await this.client.refund(
                paymentData.payment_id as string,
                Math.round(refundAmount)
            );
            return { ...paymentData, refund_id: refund.id };
        } catch (e) {
            return { error: e.message, code: 'refund_failed', detail: e };
        }
    }

    async cancelPayment(
        paymentData: Record<string, unknown>
    ): Promise<PaymentProviderError | Record<string, unknown>> {
        await this.client.cancel(paymentData.payment_id as string);
        return { ...paymentData, status: 'cancelled' };
    }

    async retrievePayment(
        paymentData: Record<string, unknown>
    ): Promise<PaymentProviderError | Record<string, unknown>> {
        const payment = await this.client.getPayment(paymentData.payment_id as string);
        return { ...paymentData, ...payment };
    }
}

export default MyPayProvider;

Регистрация провайдера

// medusa-config.ts
module.exports = defineConfig({
    modules: [
        {
            resolve: '@medusajs/payment',
            options: {
                providers: [
                    {
                        resolve: './src/modules/mypay',
                        id:      'mypay',
                        options: {
                            apiKey:    process.env.MYPAY_API_KEY,
                            secretKey: process.env.MYPAY_SECRET_KEY,
                        },
                    },
                ],
            },
        },
    ],
});

Webhook-обработчик

// src/api/mypay/webhook/route.ts
import type { MedusaRequest, MedusaResponse } from '@medusajs/framework/http';
import { ContainerRegistrationKeys } from '@medusajs/framework/utils';

export async function POST(req: MedusaRequest, res: MedusaResponse) {
    const logger = req.scope.resolve(ContainerRegistrationKeys.LOGGER);

    const signature = req.headers['x-signature'] as string;
    const isValid   = verifySignature(JSON.stringify(req.body), signature, process.env.MYPAY_SECRET_KEY!);

    if (!isValid) {
        return res.status(403).json({ message: 'Invalid signature' });
    }

    const { payment_id, status } = req.body as { payment_id: string; status: string };

    if (status === 'succeeded') {
        const paymentModuleService = req.scope.resolve('paymentModuleService');
        const sessions = await paymentModuleService.listPaymentSessions({
            data: { payment_id },
        });

        for (const session of sessions) {
            await paymentModuleService.authorizePaymentSession(session.id, req.body);
        }
    }

    res.status(200).json({ received: true });
}

Официальный Stripe провайдер

Для Stripe есть официальный @medusajs/payment-stripe:

npm install @medusajs/payment-stripe
// medusa-config.ts
{
    resolve: '@medusajs/payment-stripe',
    options: {
        apiKey:        process.env.STRIPE_API_KEY,
        webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
        capture:       true, // автоматический capture
    },
}

Официальный провайдер поддерживает Stripe webhooks, 3DS, возвраты и Stripe Connect из коробки.