Единый чекаут
Draft → finalize flow, способы оплаты, доставка, cash-on-delivery, идемпотентность, embedded YooKassa.
Единый чекаут
Чекаут в Framix — общий путь оплаты для всех каналов: товар можно купить с сайта, через чат-агента на сайте, через Telegram/VK-бота. Адрес чекаут-страницы один: /checkout/[token]. UI на сайте тоже использует ту же модалку AppCheckoutModal.
Draft → finalize
Чтобы один и тот же поток работал и для сайта, и для агента (а пользователь не заполнял адрес дважды), чекаут построен из двух шагов:
- Draft. Агент или сайт создают черновик заказа: позиции, сумма, привязка к workspace/проекту/агенту/сессии. Никаких вызовов провайдера в этот момент. Сток не блокируется. Клиент получает токен на
/checkout/<token>. - 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
- Клиент в чекауте выбирает «При получении» → finalize.
- Заказ переходит в
awaiting_cod. - Email клиенту: «Заказ оформлен, оплата при получении».
- Push владельцу: «Новый заказ под доставку».
- Бизнес доставляет, клиент платит на месте.
- В админке
/account/commerceвладелец вручную меняет статус наpaid(илиcancelled, если клиент отказался). - Сток корректируется в момент
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 (компонент) | Модалка чекаута, используется на сайтах с каталогом и на странице товара |
AppCheckoutWidget | Inline-виджет для чекаута в чате (когда агент создал бронь/заказ) |
Что хранится
Поле в commerce_order | Что |
|---|---|
status | draft → pending_payment / awaiting_cod → paid / cancelled / expired / refunded |
items | Снапшот позиций: [{ productId, name, price, quantity }] — не FK, чтобы изменения цен товара не ломали заказ |
amount | Сумма заказа в копейках |
paymentUrl | confirmation_url для redirect-режима |
confirmationToken | для embedded YooKassa |
expiresAt | TTL ссылки |
paidAt, refundedAt | моменты статусов |
Что заказчик видит после оплаты
- Email с подтверждением и кнопкой «Открыть мои заказы».
- Кнопка ведёт в
/my(вкладка «Заказы») — общий кабинет покупателя, вход по email + одноразовому коду (OTP). - В кабинете — статус заказа, кнопка повторной оплаты (для
pending_payment), история броней (/my?tab=bookings).