Skip to content

ТЗ — Брошенные корзины (cms/commerce-abandoned)

Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

Назначение и возможности

Детект брошенных корзин по неактивности с цепочкой напоминаний и статистикой возврата пользователей к покупке.

  • Детект брошенной корзины по настраиваемому таймауту неактивности
  • Цепочка напоминаний с шагами по времени (например, через 1 час / 24 часа / 3 дня)
  • События для cms/email-marketing на каждый шаг цепочки напоминаний
  • Промокод в письме-напоминании через крючок cms/coupons (опционально)
  • Статистика возврата: сколько брошенных корзин завершились заказом после напоминания
  • Исключение из детекта корзин, уже оформленных в заказ

Зависимости и выключение

requires: cms/commerce-cart · suggests: cms/email-marketing, cms/coupons

Поведение при выключении: корзины не отслеживаются на брошенность, напоминания не отправляются — сама корзина и оформление заказа работают штатно, деградация только маркетинговой функции.

Модель данных

ТаблицаКлючевые поляПримечание
commerce_abandoned_cartsid, cart_id, detected_at, reminders_sent, last_reminder_at (nullable), recovered_at (nullable)учёт брошенной корзины и статуса возврата; FK cart_idconstrained()->index(), индекс detected_at (BRIN) под джобу отправки
commerce_abandoned_reminder_stepsid, step_number, delay_minutes, coupon_id (nullable)шаг цепочки напоминаний с задержкой и опц. промокодом

last_reminder_at — для лимита частоты напоминаний (см. настройку min_reminder_interval_hours ниже). Индексы под горячий WHERE джобы send-reminders: commerce_abandoned_carts(detected_at, recovered_at) (выборка неотправленных активных).

ПДн-паспорт. Модуль не хранит email/имя сам — только cart_id (ссылка на commerce-cart, где email доступен через user_id/чекаут-контакт гостя). Срок хранения записи о брошенной корзине — до recovered_at + ретеншн-настройка (см. ниже); участие в «выгрузить всё по субъекту» — косвенное (через cart_idcommerce-cart/commerce-orders); в «забыть по запросу» — каскадное удаление своих записей по UserDeleted (через связанную корзину). Согласие на рассылку. Отправка напоминания — только при действующем согласии на маркетинговые коммуникации (проверка через cms/consents, если модуль включён; иначе согласие считается частью оформления корзины/чекаута — декларируется как открытый вопрос для конкретного сайта, если cms/consents не установлен).

Входные и выходные данные

Входы (whitelist — всё не перечисленное отклоняется 422):

ОткудаЧто приходитПоляВалидация
Джоба detect (расписание)детект брошенностиcart_id (внутренний, из commerce-cart)не пользовательский ввод — только корзины без активности дольше inactivity_timeout_minutes
Событие CartItemAdded/др. (commerce-cart)сброс пометки брошенностиcart_idpayload чужого модуля; сброс, только если корзина уже была помечена
Событие OrderCompleted/эквивалент оформления заказафиксация возвратаcart_id, order_idpayload ядра/commerce-orders; проверка «этот cart_id был в брошенных»
Filament: конструктор шаговшаг цепочкиstep_number, delay_minutes, coupon_idFormRequest-whitelist; coupon_id — только активный купон при cms/coupons включён
Ссылка восстановления в письмепереход по ссылкеsigned URL (cart_id внутри подписи, не в открытом виде)Laravel signed route + TTL, отдельно от токена корзины (см. «Безопасность»)

Выходы:

КудаФорматСодержимое
Событие CartAbandonedканал 1cart_id, detected_at
Событие AbandonedCartReminderDueканал 1cart_id, step_number, coupon_id, recovery_url (подписанная ссылка)
Событие AbandonedCartRecoveredканал 1cart_id, order_id
GET /admin/commerce-abandoned/carts{data, meta}список со статусом, email/имя маскированы (см. «Безопасность»)
GET /admin/commerce-abandoned/stats{data, meta}конверсия по шагам цепочки, агрегаты без ПДн
Filament-дашбордрендерстатистика возврата

Настройки (группа commerce-abandoned)

КлючТипДефолтaffectsPageCacheОписание
commerce-abandoned.enabledbooltrueнетВключение детекта брошенных корзин
commerce-abandoned.inactivity_timeout_minutesint60нетТаймаут неактивности до пометки корзины брошенной
commerce-abandoned.max_remindersint3нетМаксимум шагов цепочки напоминаний; при достижении — цепочка останавливается, корзина остаётся в статистике
commerce-abandoned.min_reminder_interval_hoursint12нетМинимальный интервал между напоминаниями (защита от спама, независимо от шагов цепочки)
commerce-abandoned.recovery_link_ttl_hoursint72нетСрок жизни подписанной ссылки восстановления корзины в письме
commerce-abandoned.retention_daysint180нетСрок хранения записи о брошенной корзине после recovered_at/детекта (джоба ретеншна)
commerce-abandoned.sending_enabledbooltrueнетKill-switch: отключает отправку писем-напоминаний без выключения детекта и статистики модуля

API

МетодПутьДоступНазначение
GET/api/v1/admin/commerce-abandoned/cartsadmin (commerce-abandoned.view)Список брошенных корзин со статусом возврата (keyset-пагинация)
GET/api/v1/admin/commerce-abandoned/statsadmin (commerce-abandoned.view)Статистика возврата по цепочке напоминаний

Компоненты

Filament: список брошенных корзин, конструктор шагов цепочки напоминаний, дашборд статистики возврата. Команды: cms:commerce-abandoned:detect --json, cms:commerce-abandoned:send-reminders --json, cms:commerce-abandoned:prune --json (ретеншн по retention_days). Демо-сидер: 2-3 demo-записи брошенных корзин на разных шагах цепочки для галереи Filament-дашборда и playground.

События и обмен

СобытиеКогдаPayload
CartAbandonedкорзина помечена брошенной по таймаутуcart_id, detected_at
AbandonedCartReminderDueнаступил срок отправки шага напоминанияcart_id, step_number, coupon_id
AbandonedCartRecoveredброшенная корзина завершилась заказомcart_id, order_id

Слушает: события изменения cms/commerce-cart (CartItemAdded и др.) для сброса пометки брошенности при активности; событие оформления заказа (OrderCompleted/эквивалент из commerce-orders) для фиксации AbandonedCartRecovered и остановки цепочки.

Provides-контракты: не предоставляет. FilterBus: не использует; интеграция с cms/email-marketing и cms/coupons — событиями (AbandonedCartReminderDue), не прямым вызовом.

Таблица взаимодействий:

Сущность/модульКаналНаправлениеЧто происходит
cms/commerce-cart4. requires, сервис-вызовисходящеечтение состояния корзины (позиции, updated_at) на детекте
cms/commerce-cart1. событие активности корзинывходящеесброс пометки брошенности
cms/commerce-orders (или ядро)1. событие оформления заказавходящеефиксация возврата, остановка цепочки
cms/email-marketing (suggests)1. событие AbandonedCartReminderDueисходящееотправка письма-напоминания по шагу
cms/coupons (suggests)4. requires, сервис-вызовисходящеевыпуск/валидация промокода для шага цепочки
cms/consents (если есть)4. requires/сервис-вызовисходящеепроверка действующего согласия перед каждой отправкой

Фоновая работа

Очередь commerce-abandoned: detect по расписанию через ScheduleRegistrar (детект по inactivity_timeout_minutes), send-reminders — по расписанию шагов цепочки, идемпотентно (повторный прогон не дублирует отправку), prune — ретеншн по retention_days.

Метрики и алерты: число новых брошенных корзин/сутки, конверсия возврата по шагу цепочки, доля пропущенных отправок из-за отсутствия согласия, длительность send-reminders. Алерт — send-reminders не отрабатывает 2+ расписания подряд (напоминания не уходят вовсе).

Ранбук (типовые инциденты):

СимптомЧто проверитьКоманда-лечение
Напоминания не отправляютсяcommerce-abandoned.sending_enabled (kill-switch), включён ли cms/email-marketing--dry-run на send-reminders, затем включить kill-switch обратно
Письмо ушло после того как заказ уже оформленгонка детект/оформление — проверить лог проверки «заказ не оформлен» перед отправкойне требует лечения данных — это баг слушателя, чинится в коде (см. «Крайние случаи»)
Статистика конверсии не сходится с email-marketingсверить reminders_sent и события AbandonedCartReminderDue в логе шиныcms:commerce-abandoned:detect --dry-run — сверка расхождений
Ссылка восстановления не открываетсяистёк recovery_link_ttl_hours, проверить подпись signed routeне «чинится» вручную — покупатель заходит в корзину заново; TTL — осознанный компромисс

Бэкап/рестор: commerce_abandoned_carts/commerce_abandoned_reminder_steps — в общем бэкапе БД (шаги цепочки — конфигурация, записи брошенных корзин — рабочие данные). После рестора статистика конверсии за период рестора может быть неполной (не пересчитывается автоматически) — это ожидаемо, отдельной команды восстановления не требуется.

Производительность и кеш

  • Ожидаемые объёмы: низкая частота обращения — только админка (Filament-дашборд, список), публичного горячего пути нет (кроме перехода по ссылке восстановления, который открывает обычную страницу корзины). Джобы detect/send-reminders — фоновые, не на пути запроса.
  • Кеша нет — списки брошенных корзин и статистика читаются напрямую из БД.
  • Бюджет запросов: detect и send-reminders — батчами (Bus::batch), не по одной корзине синхронным запросом; список в админке — keyset-пагинация, без OFFSET.
  • Индексы под горячий WHERE: commerce_abandoned_carts(detected_at, recovered_at) (выборка активных неотправленных на джобе), commerce_abandoned_carts(cart_id) (сброс пометки по событию активности).

Безопасность

Вход в конструктор шагов и просмотр статистики — только через FormRequest-whitelist и permissions; в статистику/список не попадают персональные данные сверх необходимого (email/имя маскируются в выдаче списка, полные данные — по клику в карточку корзины). Права: commerce-abandoned.view, commerce-abandoned.manage.

Ссылка восстановления — signed URL, не голый cart_id. Письмо содержит подписанную ссылку (Laravel signed route) с TTL (recovery_link_ttl_hours), не прямой cart_id/session_token в открытом виде — иначе пересланное письмо или лог прокси дают доступ к чужой корзине (IDOR). Истёкшая или невалидная подпись — понятная страница «ссылка устарела», не 500/белый экран.

Согласие на рассылку. Отправка каждого шага цепочки проверяет действующее согласие на маркетинговые коммуникации непосредственно перед отправкой (не только на детекте) — отзыв согласия между шагами останавливает цепочку для этой корзины.

Матрица ролей:

Рольcommerce-abandoned.viewcommerce-abandoned.manage
Администратор
Менеджер✓ (статистика и список)
Редактор
Studio✓ (конструктор цепочки)

UX-требования

Покупатель: ссылка восстановления открывает корзину в исходном состоянии без дополнительной авторизации (если TTL не истёк); истёкшая ссылка — понятное сообщение с предложением зайти в каталог заново, не техническая ошибка.

Админ: пустой список брошенных корзин — состояние «пока нет данных, детект запускается по расписанию», не пустая таблица без пояснения; конструктор шагов цепочки — массовое включение/выключение шага без пересоздания; удаление шага цепочки, на который уже запланированы отправки, — подтверждение с предупреждением «отменит N ожидающих напоминаний»; ошибки конструктора (например, coupon_id недействующего купона) — на человеческом языке с указанием, какой шаг некорректен.

Крайние случаи и типовые баги

  • Письмо о брошенной корзине после того как заказ уже оформлен (гонка детект/оформление) → проверка «заказ по этому cart_id не оформлен» выполняется непосредственно перед отправкой каждого шага цепочки (не только при детекте) — оформление заказа между шагами останавливает дальнейшую отправку немедленно, а не только на следующем прогоне джобы.
  • Частота напоминанийmin_reminder_interval_hours ограничивает отправку независимо от числа настроенных шагов; если по расчёту цепочки должно уйти два письма в течение интервала, второе откладывается до истечения интервала, не отменяется молча (лог + метрика).
  • Отозванное или отсутствующее согласие на рассылку → напоминание не отправляется ни на одном шаге без действующего согласия; корзина остаётся в статистике детекта, но помечается «не отправлено — нет согласия», не тихий пропуск без следа.
  • Ссылка восстановления протухла (TTL истёк) → переход по ссылке отдаёт понятную страницу с предложением продолжить покупки заново, не 403/500.
  • Корзина восстановлена активностью, а не переходом по ссылке (покупатель зашёл сам) → слушатель события активности commerce-cart сбрасывает пометку брошенности так же, как и переход по ссылке — единая точка сброса, не два независимых пути с расхождением.
  • max_reminders достигнут → цепочка останавливается тихо для покупателя (писем больше нет), но корзина продолжает участвовать в статистике «не вернулся после N напоминаний».
  • Выключен suggests-модуль cms/email-marketingAbandonedCartReminderDue издаётся штатно (детект и цепочка работают), но письмо физически не уходит — модуль не падает, расхождение видно в метрике «событий издано vs писем отправлено».
  • Пустая цепочка шагов (админ не настроил ни одного шага) → детект работает, но напоминания не планируются — не ошибка конфигурации, а осознанный режим «только статистика без рассылки».
  • Купон шага уже деактивирован/удалён к моменту отправки → шаг отправляется без промокода (деградация функции, не отмена всего письма), в лог — предупреждение.
  • ⚠️ Противоречие: coupon_id в commerce_abandoned_reminder_steps — конфигурация шага, общая для всех писем, а не факт применения к конкретной корзине. Аудит применения купона (кто и когда использовал) ведёт cms/coupons по своим таблицам; этот модуль не дублирует журнал использования — только ссылается на coupon_id шаблона.

Донорский код

Донор: — (новая разработка)

Миграция legacy-данных: не применимо — брошенные корзины детектируются заново после переезда клиента (исторические записи предыдущей платформы не переносят ценности без активных корзин под ними); cms:commerce-abandoned:import-legacy не требуется.

Тесты и приёмка

  • [ ] Контрактный тест: детект помечает брошенной только корзину без активности дольше таймаута
  • [ ] Оформление заказа из брошенной корзины фиксирует AbandonedCartRecovered и останавливает цепочку
  • [ ] Оформление заказа между шагами цепочки останавливает отправку следующего шага (проверка гонки на каждом шаге, не только на детекте)
  • [ ] Цепочка напоминаний не отправляет писем сверх max_reminders
  • [ ] Цепочка не отправляет два письма чаще min_reminder_interval_hours
  • [ ] Без действующего согласия на рассылку напоминание не отправляется ни на одном шаге
  • [ ] Ссылка восстановления — подписанная (signed route), не содержит голого cart_id; истёкшая ссылка отдаёт понятную страницу, не 500
  • [ ] При выключении модуля корзина и заказ работают без изменений в поведении
  • [ ] При выключенном cms/email-marketing событие издаётся, но отправка не падает и не блокирует детект
  • [ ] Права commerce-abandoned.view/.manage разграничивают статистику и настройку цепочки
  • [ ] Контрактный набор cms-testing и testbench-изоляция зелёные, feature-тест на каждый роут
  • [ ] Тестовая БД только commerce-abandoned_test; migrate:fresh/refresh/reset запрещены

Внутренняя база знаний студии