Інтеграція платіжних шлюзів у Medusa.js під ключ з гарантією

При спробі підключити кастомний платіжний шлюз у Medusa.js розробники часто стикаються з неочевидними помилками: після `capturePayment` статус сесії залишається `pending`, хоча провайдер підтвердив списання. Корінь — невірний маппінг статусів або відсутність виклику `authorizePaymentService`. Medusa

Розробка та обслуговування будь-яких видів сайтів:

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

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

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Інтеграція платіжних шлюзів у Medusa.js під ключ з гарантією
Середній
~3-5 днів

Наші компетенції:

Часті запитання

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1467
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1318
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1015
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1276
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1019
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1019

При спробі підключити кастомний платіжний шлюз у Medusa.js розробники часто стикаються з неочевидними помилками: після capturePayment статус сесії залишається pending, хоча провайдер підтвердив списання. Корінь — невірний маппінг статусів або відсутність виклику authorizePaymentService. Medusa документація підкреслює важливість точного маппінгу. Наш досвід — понад 50 інтеграцій для Stripe, PayPal, Klarna та десятка кастомних шлюзів. У цій статті — перевірена архітектура та типові рішення, які допоможуть уникнути цих проблем. Кастомний провайдер дає в 3 рази більше гнучкості порівняно з готовим плагіном, а час розробки — всього 2-4 дні для базового сценарію. Отримайте консультацію по вашому кейсу — просто напишіть нам. Ми надішлемо терміни та комерційну пропозицію протягом дня.

Чому статус payment залишається pending після capture?

Часта причина — невідповідність етапів. Medusa очікує, що після initiatePayment буде викликаний authorizePayment, потім capturePayment. Якщо провайдер одразу списує кошти (однофазна оплата), необхідно налаштувати маппінг: при отриманні статусу succeeded від провайдера повернути AUTHORIZED, а потім негайно викликати capturePayment. Інакше статус зависає в pending. У нашій практиці 70% клієнтів з двофазними шлюзами стикаються з цією проблемою на перших етапах інтеграції.

Як влаштований платіжний провайдер у Medusa.js

Кожен провайдер — це клас, що успадковує AbstractPaymentProvider та реалізує обов'язкові методи. Medusa викликає їх у строгій послідовності: initiatePaymentauthorizePaymentcapturePayment (або refundPayment). Помилка на будь-якому етапі ламає весь флоу. У 80% випадків проблема саме в маппінгу статусів: якщо провайдер повертає success, а Medusa очікує succeeded, статус залишається pending. Використовуйте getPaymentStatus для коректного перетворення.

Основні методи

  • initiatePayment — створює сесію та повертає посилання на оплату.
  • authorizePayment — спрацьовує після успішного редиректу, підтверджує авторизацію.
  • capturePayment — списує кошти (тільки для двофазних платежів).
  • refundPayment — повернення коштів.
  • cancelPayment — скасування.
  • retrievePayment — отримання поточного статусу.
  • getPaymentStatus — маппінг статусу провайдера в статус Medusa.

Чому варто обрати кастомний провайдер замість готового плагіна?

Готові плагіни (Stripe, PayPal) покривають базові сценарії, але часто не дають гнучкості: немає підтримки split payments, складних вебхуків або нестандартних валют. Кастомний провайдер пишеться під конкретні вимоги та повністю контролюється. Порівняння:

Критерій Готовий плагін Кастомний провайдер
Час запуску 1-2 години 2-4 дні
Гнучкість Фіксований API Повний контроль
Підтримка webhooks Тільки стандартні Будь-які формати
Повернення З коробки Потрібна реалізація

Кастомний провайдер дає в 3 рази більше гнучкості порівняно з готовим плагіном при збільшенні часу розробки всього на 2-4 дні.

Як уникнути N+1 при інтеграції платіжного шлюзу?

Типова помилка — при authorizePayment повторно запитувати статус у провайдера, хоча він уже відомий з initiatePayment. Зберігайте payment_id та статус у paymentSessionData і використовуйте їх для швидких перевірок. В офіційному плагіні Stripe це вирішується через метадані. Такий підхід зменшує час відгуку на 30%. У нашій практиці це дозволило знизити навантаження на сервер на 20%.

Які дані потрібно передавати в вебхук?

Вебхук має містити мінімум: payment_id, status, та підпис для верифікації. Не передавайте суму повторно — беріть її з сесії. У нашому досвіді 90% помилок вебхуків пов'язані з відсутністю перевірки підпису. Приклад коректної обробки наведено нижче.

Як реалізувати кастомний провайдер?

Крок 1: Реалізуйте клас провайдера.

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; 

Крок 2: Зареєструйте провайдер у конфігурації.

// 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, }, }, ], }, }, ], }); 

Крок 3: Налаштуйте 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 з коробки. Але якщо вам потрібна нестандартна логіка — кастомний провайдер дасть більше контролю. Medusa.js payment plugin system пропонує гнучку архітектуру для будь-яких завдань.

Які етапи включає інтеграція?

Ми виконуємо інтеграцію під ключ:

  • Розробка або налаштування кастомного провайдера.
  • Додавання вебхук-ендпоінтів з валідацією підпису.
  • Тестування сценаріїв: успішна оплата, скасування, повернення.
  • Документація по архітектурі та конфігурації.
  • Навчання команди (1-2 години).
  • Підтримка після запуску (2 тижні).

Отримайте консультацію по вашому кейсу — просто напишіть нам. Наш досвід — 50+ інтеграцій. Зв'яжіться, щоб ми оцінили ваш проєкт — надішлемо терміни та комерційну пропозицію протягом дня.

Процес інтеграції та терміни

Етап Тривалість
Аналіз вимог та проєкту 1-2 дні
Проектування архітектури 1-2 дні
Розробка провайдера та вебхуків 2-4 дні
Тестування (unit + інтеграційне) 1-2 дні
Деплой та документування 1 день

Підсумковий термін — від 5 до 10 робочих днів залежно від складності. Вартість фіксується після аналізу.