Search for a command to run...
Для интеграции платёжного провайдера Robokassa в storefront Medusa DTC Starter от Gorgo начните с добавления необходимых UI-компонентов. Таким образом провайдер будет отображаться на странице оформления заказа наряду с другими доступными методами оплаты.
Когда покупатель выбирает Robokassa, оформление заказа инициализирует платёжную сессию, а провайдер создаёт её на стороне Robokassa и возвращает адрес для оплаты. Кнопка оплаты отправляет покупателя на эту страницу, где он сможет выбрать предпочтительный способ оплаты.
После завершения оплаты Robokassa одновременно отправляет вебхук и перенаправляет покупателя обратно в витрину. То событие, которое придёт первым, завершит корзину и создаст новый заказ в Medusa.
Для витрины на Next.js необходимо внести следующие изменения:
Чтобы сделать Robokassa доступным в качестве способа оплаты на странице оформления заказа витрины магазина, необходимо добавить её конфигурацию в маппинг платёжных провайдеров в файле с константами вашей витрины. Этот маппинг определяет, как каждый провайдер отображается в интерфейсе.
Откройте и добавьте следующий код:

src/lib/constants.tsx1export type PaymentInfo = {2 title: string3 subtitle?: string4 icon: React.JSX.Element5}67export const paymentInfoMap: Record<string, PaymentInfo> = {8 // ...9 pp_robokassa_robokassa: {10 title: "Robokassa",11 subtitle: "Cards, SBP",12 icon: <Robokassa />,13 },14 // ...15}1617// ...1819export const isRobokassa = (providerId?: string) => {20 return providerId?.startsWith("pp_robokassa")21}
Вы расширяете объект , добавляя в него запись . Эта запись определяет заголовок, подпись и иконку, которые будут отображаться для Robokassa на странице оформления заказа, а также служит резервным значением, когда для локали нет перевода.
Вспомогательная функция проверяет, принадлежит ли переданный к Robokassa. Она используется диспетчером ниже и функцией сборки данных платёжной сессии из следующего шага.
Добавьте компонент для Robokassa:
src/modules/common/icons/robokassa.tsx1import type React from "react"23import type { IconProps } from "types/icon"45const Robokassa: React.FC<IconProps> = ({6 size = "16",7 color: _color,8 ...attributes9}) => {10 return (11 <svg12 width={size}13 height={size}14 viewBox="54 129 404 254"15 role="img"16 fill="none"17 xmlns="http://www.w3.org/2000/svg"18 {...attributes}19 >20 <title>Robokassa icon</title>21 <path d="M239 129L54 383H274L458 129H239Z" fill="#333339" />22 </svg>23 )24}2526export default Robokassa
Этот компонент передаётся как в записи . Он отображает иконку способа оплаты Robokassa на странице оформления заказа.
Чтобы перенаправить покупателя в Robokassa, платёжная сессия должна быть корректно инициализирована с необходимыми параметрами, включая адрес возврата после оплаты и корзину для формирования онлайн-чеков.
Откройте тот же файл и добавьте вспомогательную функцию активной локали, адрес возврата и функцию сборки данных сессии:

src/lib/constants.tsx1/**2 * Локаль, в которой отображалось оформление заказа. `<html lang>` хранит её,3 * поэтому страница оплаты и возврат с неё остаются на том языке, на котором4 * покупатель ушёл из магазина.5 */6const activeLocale = () =>7 typeof document === "undefined"8 ? defaultLocale9 : document.documentElement.lang || defaultLocale1011const paymentReturnUrl = (providerId: string, cartId: string) => {12 const origin =13 typeof window === "undefined" ? getBaseURL() : window.location.origin1415 return `${origin}/api/payment-return?provider=${providerId}&cart_id=${cartId}&locale=${activeLocale()}`16}1718const paymentSessionDataBuilders: Array<{19 test: (providerId?: string) => boolean | undefined20 isReady?: (cart: HttpTypes.StoreCart) => boolean21 build: (cart: HttpTypes.StoreCart) => Record<string, unknown>22}> = [23 {24 test: (providerId) => isRobokassa(providerId),25 // Robokassa формирует чек из корзины и берёт из неё адрес покупателя.26 // Инициализация сессии до того, как оформление заказа собрало эти данные,27 // создаёт платёж без чека.28 isReady: (cart) => Boolean(cart.email && cart.shipping_address),29 build: (cart) => {30 const returnUrl = paymentReturnUrl("robokassa", cart.id)3132 return {33 SuccessUrl2: returnUrl,34 SuccessUrl2Method: "GET",35 FailUrl2: returnUrl,36 FailUrl2Method: "GET",37 EMail: cart.email,38 // Собственная страница оплаты Robokassa говорит по-русски или по-английски.39 Culture: activeLocale() === "ru" ? "ru" : "en",40 cart,41 }42 },43 },44]
Robokassa нужна активная локаль не только для адреса возврата, поэтому она вынесена в отдельную функцию , а не встроена напрямую. передаёт и , оба указывающие на один и тот же адрес возврата, их , email покупателя в и для языка, на котором откроется собственная страница оплаты Robokassa.
Оба адреса указывают на API-роут вашей витрины и используются как для успешных, так и для неудачных попыток оплаты.
Объект включается в данные инициализации для формирования чека в соответствии с Федеральным законом № 54.
удерживает сессию, пока в корзине не появятся email и адрес доставки, поскольку для чека нужен адрес покупателя.
В витрине для каждого платёжного провайдера необходим отдельный компонент кнопки оплаты. Используя данные платёжной сессии, он перенаправляет покупателя на страницу оплаты Robokassa. Сам заказ оформляется позже, когда покупатель вернётся через , либо раньше по вебхуку, если тот придёт первым.
Создайте файл и добавьте следующий код:

src/modules/checkout/components/payment-button/providers/robokassa.tsx1"use client"23import type { HttpTypes } from "@medusajs/types"4import { useTranslations } from "next-intl"5import { useState } from "react"6import { PaymentButtonShell, type PaymentProviderButtonProps } from "../shared"78const paymentUrl = (cart: HttpTypes.StoreCart) => {9 const session = cart.payment_collection?.payment_sessions?.find((s) =>10 s.provider_id.startsWith("pp_robokassa")11 )1213 return (session?.data as { paymentUrl?: string } | undefined)?.paymentUrl14}1516/**17 * Robokassa принимает оплату на собственной странице. Сессия, созданная18 * оформлением заказа, хранит подписанный адрес, куда отправить покупателя, а19 * заказ оформляется, когда он вернётся через `/api/payment-return`, либо по20 * вебхуку, если тот придёт раньше.21 */22const RobokassaPaymentButton = ({23 cart,24 notReady,25 label,26 cartUpdating,27 "data-testid": dataTestId,28}: PaymentProviderButtonProps) => {29 const t = useTranslations("Errors")30 const [submitting, setSubmitting] = useState(false)31 const [errorMessage, setErrorMessage] = useState<string | null>(null)3233 const handlePayment = () => {34 const url = paymentUrl(cart)3536 if (!url) {37 setErrorMessage(t("paymentFailed"))38 return39 }4041 setSubmitting(true)42 window.location.assign(url)43 }4445 return (46 <PaymentButtonShell47 label={label}48 onClick={handlePayment}49 disabled={notReady}50 loading={submitting || cartUpdating}51 errorMessage={errorMessage}52 errorTestId="robokassa-payment-error-message"53 data-testid={dataTestId ?? "submit-order-button"}54 />55 )56}5758export default RobokassaPaymentButton
Этот компонент находит Robokassa в активной корзине и извлекает значение . Когда покупатель нажимает кнопку оплаты, он перенаправляется на этот URL для завершения оплаты на странице Robokassa.
Если отсутствует, кнопка показывает сообщение об ошибке вместо перехода.
Импортируйте кнопку и в , затем добавьте для неё в :

src/modules/checkout/components/payment-button/index.tsx1import {2 isManual,3 isPaymentSessionReady,4 isRobokassa,5 isStripeLike,6} from "@lib/constants"7// ...8import RobokassaPaymentButton from "./providers/robokassa"910// ...1112 switch (true) {13 // ...14 case isRobokassa(activePaymentMethod):15 return <RobokassaPaymentButton {...providerProps} />16 default:17 // ...18 }
Родительский компонент использует , чтобы определить, нужно ли отобразить для текущей сессии, иначе выполнение переходит к следующей ветке.
После того как покупатель завершает оплату на странице Robokassa, он перенаправляется обратно на витрину магазина. Robokassa сообщает статус платежа и через это перенаправление, и через вебхук, поэтому общий API-роут проверяет, есть ли у корзины заказ, прежде чем оформлять его самому.
Откройте со следующим содержимым:

src/app/api/payment-return/route.ts1type ReturnContext = {2 cartId: string3 locale: string4 origin: string5}67/**8 * Обрабатывает возврат от провайдера, который принимает оплату на собственной9 * странице и возвращает покупателя только с идентификатором корзины —10 * Robokassa в их числе.11 *12 * Вебхук провайдера и этот редирект соревнуются друг с другом, и какой из них13 * ни пришёл бы первым, он оформляет заказ, поэтому перед оформлением14 * проверяется, нет ли у корзины уже заказа.15 */16async function redirectProviderReturn({17 cartId,18 locale,19 origin,20}: ReturnContext) {21 const cart = await sdk.client22 .fetch<HttpTypes.StoreCartResponse>(`/store/carts/${cartId}`, {23 method: "GET",24 query: { fields: "id,order_link.order_id" },25 headers: { ...(await getAuthHeaders()) },26 cache: "no-store",27 })28 .then(({ cart }) => cart)29 .catch(() => null)3031 if (!cart) {32 return NextResponse.redirect(33 `${origin}/${locale}/cart?error=payment_failed`34 )35 }3637 const orderId = (cart as unknown as { order_link?: { order_id?: string } })38 .order_link?.order_id3940 if (orderId) {41 const orderCacheTag = await getCacheTag("orders")42 revalidateTag(orderCacheTag)43 await removeCartId()4445 return NextResponse.redirect(46 `${origin}/${locale}/order/${orderId}/confirmed`47 )48 }4950 // Покупатель отменил оплату, либо она была отклонена, поэтому он возвращается51 // на страницу оформления заказа с нетронутой корзиной и может выбрать способ52 // оплаты заново.53 await setCartId(cartId)5455 try {56 await placeOrder(cartId)57 } catch (error) {58 unstable_rethrow(error)5960 return NextResponse.redirect(61 `${origin}/${locale}/checkout?error=payment_failed`62 )63 }6465 return NextResponse.redirect(66 `${origin}/${locale}/checkout?error=payment_failed`67 )68}
Затем в функции направьте возврат Robokassa в :
src/app/api/payment-return/route.ts1if (provider === "robokassa") {2 return redirectProviderReturn({ cartId, locale, origin })3 }
Этот API-роут обрабатывает возврат от Robokassa после попытки оплаты. Он всегда получает актуальное состояние корзины без кэша, чтобы не пропустить изменения, внесённые во время оплаты.
Если у корзины уже есть заказ, значит подтверждение уже пришло по вебхуку. API-роут обновляет кэш заказа, удаляет cookie корзины и отправляет покупателя на страницу подтверждения. Это гарантирует корректное завершение платёжного процесса и сохранение актуальных данных в витрине.
Иначе API-роут пытается оформить заказ сам. В случае успеха покупатель оказывается на странице подтверждения заказа. Если же возникает ошибка, покупатель возвращается на оформление заказа с и может выбрать способ оплаты заново.
Добавьте в и в каждую другую локаль под :
messages/ru.json1{2 "PaymentMethods": {3 // ...4 "pp_robokassa_robokassa": {5 "title": "Робокасса",6 "subtitle": "Карты, СБП"7 }8 }9}
Если для локали нет записи, используются английские и из в Шаге 1.
Полный код витрины находится в , собранной на основе Medusa DTC Starter от Gorgo. В README примера перечислены все файлы, из которых состоит интеграция, включая бэкенд.