Единый чекаут
Draft → finalize flow, настраиваемые поля формы, доставка с расчётом на сервере, cash-on-delivery, embedded YooKassa, идемпотентность.
Единый чекаут
Чекаут в Framix — общий путь оплаты для всех каналов: товар можно купить с сайта, через AI-агента на сайте, через Telegram/VK. Адрес чекаут-страницы один: /checkout/[token]. На сайте используется та же модалка AppCheckoutModal.
Draft → finalize
Чтобы один поток работал и для сайта, и для агента (пользователь не заполняет адрес дважды), чекаут построен из двух шагов:
- Draft. Агент или сайт создают черновик заказа: позиции, сумма, привязка к группе/проекту/агенту/сессии. Никаких вызовов провайдера. Сток не блокируется. Клиент получает токен на
/checkout/<token>. - Finalize. Клиент на чекауте выбирает способ оплаты (
Картой онлайн/При получении), доставку, заполняет контакты — и финализирует одним кликом. В этот момент:- сток лочится (
SELECT FOR UPDATEпо позициям); - тиры цен и скидки от суммы пересчитываются сервером (клиентским данным не доверяем);
- создаётся payment в провайдере (для card-online);
- возвращается
confirmation_token(для embedded YooKassa) илиconfirmation_url(redirect).
- сток лочится (
Финализация идемпотентна: гонка вкладок, double-submit, retry-после-ошибки-сети вернут ту же платёжную ссылку. Никаких дублей.
Настраиваемые поля формы
/account/checkout-fields — единая конфигурация для всей группы. Применяется и к товарным заказам, и к оплачиваемым броням услуг.
Встроенные поля:
- Email — обязателен всегда (54-ФЗ: нужен для чека). Убрать нельзя.
- Имя и Телефон — можно скрыть или сделать обязательными.
Кастомные поля (до 20 штук):
- Типы:
text(однострочный),textarea(многострочный),select(список). - Готовые пресеты: Город, ИНН, Промокод, Адрес доставки и другие.
- Порядок drag-and-drop.
Все значения сохраняются снапшотом в commerce_order.customer_fields / booking.customer_fields и отображаются владельцу в drawer'е заказа и карточках броней.
Способы оплаты
| Способ | Когда | Что происходит |
|---|---|---|
| Картой онлайн | Всегда | Создаётся payment в провайдере, клиент платит. Embedded-виджет (YooKassa) или redirect (остальные). |
| При получении (cash-on-delivery) | Если в корзине есть товар с requiresShipping=true | Заказ переходит в awaiting_cod. Провайдеру ничего не отправляется. Бизнес доставляет товар и принимает оплату на месте. |
Доставка
Если хоть один товар в корзине требует доставки (requiresShipping=true), на чекауте появляется блок выбора метода.
Методы доставки (настройка в кабинете)
/account/commerce → вкладка «Доставка» (AppShippingSettings):
| Тип метода | Описание |
|---|---|
| Самовывоз | Одна или несколько точек выдачи с адресом. Клиент выбирает точку. |
| Фиксированный тариф | Одна цена для всех. Опция «бесплатно от суммы заказа X ₽». |
| Зональный | Города → своя цена и срок. Поддерживает зону-fallback («все остальные города»). «По городу бесплатно» — отдельный флаг. |
Все методы редактируются в диалоге AppShippingMethodDialog. Внешние перевозчики (СДЭК, Boxberry) — будущая фаза, интерфейс адаптера уже готов.
Чекаут клиента
- Автоопределение города по IP (сервис
ipwho.is, best-effort). - Автокомплит городов из бандлованного списка городов РФ — без внешних ключей.
- Клиент выбирает метод (с ценой и сроком) и, для самовывоза, точку.
Расчёт на сервере
finalize пересчитывает стоимость доставки по выбранному методу (quoteSingleMethod) — клиентскому deliveryCost сервер не доверяет. В заказ пишется снапшот: метод, точка самовывоза, срок доставки (commerce_order.shipping_method_*, pickup_point_id, delivery_eta_*).
API доставки: GET /api/shipping/{methods,quote,cities,detect-city}.
Cash-on-delivery flow
- Клиент выбирает «При получении» → finalize.
- Заказ переходит в
awaiting_cod. - Email клиенту: «Заказ оформлен, оплата при получении».
- Push владельцу: «Новый заказ под доставку».
- Бизнес доставляет, клиент платит на месте.
- Владелец вручную меняет статус на
paid(илиcancelled). - Сток корректируется в момент
paid.
Embedded YooKassa
После finalize, если provider = YooKassa, возвращается confirmation_token. На чекаут-странице рендерится YooMoneyCheckoutWidget — клиент платит, не уходя со страницы. Конверсия выше, чем с redirect, на 5–15%.
Для остальных провайдеров используется confirmation_url (классический redirect). В Telegram/VK — только redirect.
Embedded работает на host'е framix.app и на любых ваших custom-доменах — без дополнительной настройки.
Cron auto-expire
Брошенные draft'ы (клиент открыл чекаут, ушёл, не оплатил) переходят в expired после TTL (раз в час):
- Сток больше не блокируется.
- Статистика по конверсии не загрязняется.
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 | Сумма в копейках |
discount_amount | Скидка от суммы корзины в копейках |
discount_percent | Процент примененной скидки |
customer_fields | Значения кастомных полей формы (JSONB) |
shipping_method_type | Выбранный метод доставки |
pickup_point_id | Точка самовывоза (если выбрана) |
delivery_eta_min/max | Срок доставки в днях |
paymentUrl | confirmation_url для redirect-режима |
confirmationToken | Для embedded YooKassa |
expiresAt | TTL ссылки |
paidAt, refundedAt | Моменты смены статуса |
Что видит клиент после оплаты
- Email с подтверждением и кнопкой «Открыть мои заказы».
- Кнопка ведёт в
/my(вкладка «Заказы») — общий кабинет покупателя, вход по email + OTP. - В кабинете — статус заказа, кнопка повторной оплаты (для
pending_payment), история броней.