Единый чекаут

Draft → finalize flow, способы оплаты, доставка, cash-on-delivery, идемпотентность, embedded YooKassa.

Единый чекаут

Чекаут в Framix — общий путь оплаты для всех каналов: товар можно купить с сайта, через чат-агента на сайте, через Telegram/VK-бота. Адрес чекаут-страницы один: /checkout/[token]. UI на сайте тоже использует ту же модалку AppCheckoutModal.

Draft → finalize

Чтобы один и тот же поток работал и для сайта, и для агента (а пользователь не заполнял адрес дважды), чекаут построен из двух шагов:

  1. Draft. Агент или сайт создают черновик заказа: позиции, сумма, привязка к workspace/проекту/агенту/сессии. Никаких вызовов провайдера в этот момент. Сток не блокируется. Клиент получает токен на /checkout/<token>.
  2. Finalize. Клиент на чекауте выбирает способ оплаты (Картой онлайн / При получении), доставку (pickup / courier / post / other), заполняет контакты — и финализирует одним кликом. В этот момент:
    • сток лочится (SELECT FOR UPDATE по позициям);
    • создаётся payment в провайдере (для card-online);
    • возвращается confirmation_token (для embedded YooKassa) или confirmation_url (для redirect).

Финализация идемпотентна: гонка вкладок, double-submit, retry-после-ошибки-сети вернут ту же платёжную ссылку. Никаких дублей.

Способы оплаты

СпособКогдаЧто происходит
Картой онлайнВсегдаСоздаётся payment в провайдере, клиент платит. Embedded-виджет (YooKassa) или redirect (остальные).
При получении (cash-on-delivery)Если в корзине есть товар с requiresShipping=trueЗаказ переходит в awaiting_cod. Провайдеру ничего не отправляется. Бизнес доставляет товар и принимает оплату на месте.

После выбора cash-on-delivery клиенту приходит email с подтверждением, в чате агента уходит сообщение об оформлении, владельцу — push-уведомление о новом заказе.

Доставка

Если хоть один товар в корзине требует доставки (requiresShipping=true), на чекауте появляется блок выбора:

  • pickup — самовывоз;
  • courier — курьер;
  • post — Почта России / СДЭК / Boxberry — выбор курьерки на стороне бизнеса (Framix не интегрируется с CDEK напрямую);
  • other — произвольный вариант.

Адрес — текстовое поле; геокодирование не делается. Параметры доставки сохраняются в commerce_order.shipping (JSONB).

Если ни один товар не требует доставки (например, услуги, цифровые товары) — блок не показывается, заказ оформляется без адреса.

Cash-on-delivery flow

  1. Клиент в чекауте выбирает «При получении» → finalize.
  2. Заказ переходит в awaiting_cod.
  3. Email клиенту: «Заказ оформлен, оплата при получении».
  4. Push владельцу: «Новый заказ под доставку».
  5. Бизнес доставляет, клиент платит на месте.
  6. В админке /account/commerce владелец вручную меняет статус на paid (или cancelled, если клиент отказался).
  7. Сток корректируется в момент paid.

Embedded YooKassa

После finalize, если provider = YooKassa, возвращается confirmation_token. На чекаут-странице рендерится YooMoneyCheckoutWidget — клиент платит, не уходя со страницы. Конверсия выше, чем с redirect, на 5-15%.

Для остальных провайдеров используется confirmation_url (классический redirect).

Embedded работает on host'е framix.app и на любых ваших custom-доменах — никакой дополнительной настройки. YooKassa проверяет origin запроса, и доверенные origin'ы рег устанавливаются по факту custom_domain'а проекта.

Cron auto-expire

Брошенные draft'ы (клиент открыл чекаут, ушёл, не оплатил) — переходят в expired после TTL (раз в час). Это нужно, чтобы:

  • сток в pending_payment не блокировал каталог бесконечно;
  • статистика по конверсии не загрязнялась «висячими» заказами.

pending_payment со ссылкой на провайдера не трогаем — поздний webhook должен иметь возможность долететь и перевести в paid.

Где именно живёт чекаут

URL / компонентНазначение
/checkout/[token]Публичная страница чекаута (десктоп/мобайл)
AppCheckoutModal (компонент)Модалка чекаута, используется на сайтах с каталогом и на странице товара
AppCheckoutWidgetInline-виджет для чекаута в чате (когда агент создал бронь/заказ)

Что хранится

Поле в commerce_orderЧто
statusdraftpending_payment / awaiting_codpaid / cancelled / expired / refunded
itemsСнапшот позиций: [{ productId, name, price, quantity }] — не FK, чтобы изменения цен товара не ломали заказ
amountСумма заказа в копейках
paymentUrlconfirmation_url для redirect-режима
confirmationTokenдля embedded YooKassa
expiresAtTTL ссылки
paidAt, refundedAtмоменты статусов

Что заказчик видит после оплаты

  • Email с подтверждением и кнопкой «Открыть мои заказы».
  • Кнопка ведёт в /my (вкладка «Заказы») — общий кабинет покупателя, вход по email + одноразовому коду (OTP).
  • В кабинете — статус заказа, кнопка повторной оплаты (для pending_payment), история броней (/my?tab=bookings).

На этой странице