Skip to content

ТЗ — Доставка РФ (СДЭК/Почта/Boxberry) (cms/delivery-ru)

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

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

Коннекторы российских служб доставки (СДЭК, Почта России, Boxberry) поверх cms/integrations-bus: реализуют контракт DeliveryProvider из cms/commerce-delivery, дают checkout расчёт стоимости/сроков и карту ПВЗ без привязки к конкретной службе.

  • Коннекторы СДЭК, Почта России, Boxberry: provides: delivery-provider
  • Расчёт стоимости и сроков доставки по адресу/габаритам заказа
  • Виджет карты ПВЗ на checkout с выбором пункта выдачи
  • Создание отправления и получение номера для отслеживания
  • Трек-статусы — опрос по расписанию из очереди, не по запросу пользователя
  • Печать этикеток отправления из Filament

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

requires: cms/integrations-bus, cms/commerce-delivery · provides: delivery-provider

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

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

ТаблицаКлючевые поляПримечание
cms_delivery_ru_shipmentsid, order_id, provider_code, tracking_number, status, label_urlотправление, привязанное к заказу
cms_delivery_ru_pickup_pointsid, provider_code, external_id, address, coordinates (json)кеш ПВЗ для виджета карты

Индексы: FK order_idconstrained() + index(); status — PHP Enum; coordinatesjson() + cast array; индекс на provider_code+external_id (поиск ПВЗ при синхронизации); индекс status на cms_delivery_ru_shipments (выборка «нужен опрос» пачками — см. «Производительность и кеш»).

cms_delivery_ru_shipments не хранит ФИО/адрес/телефон получателя отдельными столбцами — при создании отправления они читаются из заказа (через cms/commerce-delivery) и передаются провайдеру транзитом; локально сохраняются только служебные идентификаторы (tracking_number, label_url) — см. ПДн-паспорт в «Безопасность».

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

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

ВходИсточникКаналПоляПроверка
Расчёт стоимости/сроковcheckout (auth/guest)API POST /checkout/delivery/estimateадрес/индекс, вес/габариты заказа, provider_code?FormRequest whitelist; адрес нормализуется через dadata (suggest-provider), если включён (см. «Крайние случаи»)
Список ПВЗ для картыcheckout (auth/guest)API GET /checkout/delivery/pickup-pointsадрес/координаты/радиус, provider_code?whitelist параметров, читается из кеша справочника
Создание отправленияcms/commerce-delivery (после подтверждения заказа)канал 4 (requires), ShipmentService::create()order_id, provider_code, вес/габариты, адрес/ПВЗсверка с данными заказа
Печать этикеткиadminAPI POST /shipments/{id}/labeliddelivery-ru.manage, проверка активного отправления, Idempotency-Key
Вебхук трек-статусавнешняя служба (СДЭК/Почта/Boxberry)cms/webhooks-incms/integrations-bustracking_number, provider_code, status, timestampподпись (HMAC, секрет per-провайдер из .env) обязательна; протухший timestamp отклоняется; неизвестный tracking_number логируется и отклоняется
Ответ синхронизации ПВЗвнешний API провайдера (по расписанию)канал 5 (очередь) + integrations-busсписок точек (координаты, адрес, external_id)whitelist полей, координаты валидируются перед записью
ВыходПолучательФормат
Ответ расчёта доставкиcheckout{data, meta}, meta.degraded: true при фолбэке (см. «Крайние случаи»)
Ответ списка ПВЗcheckout{data, meta}, keyset
DeliveryShipmentCreated / DeliveryStatusChanged / DeliveryPickupPointsSyncedподписчики (канал 1)события ядра
Этикетка отправленияadmin (печать/скачивание)label_url (файл провайдера, PDF/PNG)

Настройки (группа delivery-ru)

КлючТипДефолтaffectsPageCacheОписание
delivery-ru.enabled_providersarray[]нетАктивные коды служб доставки
delivery-ru.pickup_points_sync_hoursint24нетПериодичность синхронизации справочника ПВЗ
delivery-ru.tracking_poll_minutesint60нетПериодичность опроса трек-статусов
delivery-ru.tracking_poll_batch_sizeint200нетРазмер пачки опроса за один прогон — лимит против rate-limit провайдера
delivery-ru.disabled_providersarray[]нетKill-switch: коды служб, временно исключённых из checkout без выключения модуля
delivery-ru.estimate_timeout_msint2000нетТаймаут синхронного расчёта стоимости на checkout (бюджет ≤2 с, стандарт §9)
delivery-ru.estimate_fallback_policyenum (defer|fixed_rate|hide)deferнетПолитика при недоступности расчёта — см. «Крайние случаи»
delivery-ru.fixed_rate_amountint (minor units)nullнетСумма фолбэка при политике fixed_rate
delivery-ru.pickup_points_max_age_hoursint72нетПорог возраста кеша ПВЗ для предупреждения в админке
delivery-ru.price_per_requestdecimalnullнетСправочная стоимость API-вызова у провайдера (видимость расхода)

Секреты доступа к API служб доставки — только .env.

API

МетодПутьДоступНазначение
POST/api/v1/checkout/delivery/estimateauth/guestРасчёт стоимости и сроков по адресу
GET/api/v1/checkout/delivery/pickup-pointsauth/guestСписок ПВЗ для виджета карты
GET/api/v1/admin/delivery-ru/shipmentsadmin (delivery-ru.view)Список отправлений с трек-статусами
POST/api/v1/admin/delivery-ru/shipments/{id}/labeladmin (delivery-ru.manage)Печать этикетки отправления

Список ПВЗ и отправлений — keyset-пагинация, не OFFSET. {id} в пути печати этикетки — публичный идентификатор отправления (ULID/UUID первичного ключа), не автоинкремент — см. «Крайние случаи» (⚠️ Противоречие со стандартом §4, если реализация выберет обычный id). Создание отправления и печать этикетки — мутации с внешним эффектом: заголовок Idempotency-Key обязателен (стандарт §7).

Компоненты

Блоки: виджет карты ПВЗ на checkout. Filament: ресурс отправлений с трек-статусами, печать этикеток. Команды: cms:delivery-ru:sync-pickup-points --json, cms:delivery-ru:poll-tracking --json.

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

СобытиеКогдаPayload
DeliveryShipmentCreatedотправление создано у службы доставкиorder_id, provider_code, tracking_number
DeliveryStatusChangedизменился трек-статус отправленияshipment_id, old_status, new_status
DeliveryPickupPointsSyncedсправочник ПВЗ обновлёнprovider_code, points_count

Реализует provides: delivery-provider — потребитель cms/commerce-delivery. Обмен со службами доставки — через cms/integrations-bus.

Взаимодействия

Сущность/модульКаналНаправлениеЧто происходит
cms/commerce-delivery3 (provides: delivery-provider)commerce-delivery → delivery-ruрезолв реализации DeliveryProvider на checkout
cms/commerce-delivery4 (requires)delivery-ru → commerce-deliveryчтение данных заказа/веса при создании отправления
checkout (публичный API)внешний канал (REST)checkout → delivery-ruсинхронный расчёт стоимости/сроков, список ПВЗ
Служба доставки (внешняя)5 (очередь) + cms/integrations-busdelivery-ru → внешний APIсоздание отправления, синхронизация ПВЗ, опрос трек-статусов
Служба доставки → delivery-ru (вебхук)внешний канал (webhooks-inintegrations-bus)внешний → delivery-ruсмена трек-статуса, подпись обязательна
cms/notifications-bus (если включён)3 (контракт)delivery-ru → notificationsDeliveryStatusChanged → письмо/пуш клиенту
dadata (suggest-provider, если включён)3 (сервисный контракт)delivery-ru → dadataнормализация адреса перед расчётом (см. «Крайние случаи»)
cms/healthhealth-чек + событие 1delivery-ru → healthнедоступность провайдера, устаревание справочника ПВЗ

Любая связь вне этой таблицы — скрытая зависимость, анти-паттерн (стандарт §5).

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

Синхронизация справочника ПВЗ — по расписанию (pickup_points_sync_hours), именованная очередь delivery-ru. Опрос трек-статусов — по расписанию (tracking_poll_minutes), пачками по tracking_poll_batch_size, не по запросу пользователя. Расчёт стоимости на checkout — синхронный вызов с таймаутом estimate_timeout_ms и graceful fallback (не блокирует оформление). Ретраи с backoff, circuit breaker на провайдера. Восстановительные команды (sync-pickup-points, poll-tracking) — идемпотентны, --json, допустимы к ручному прогону на живом сайте (стандарт §15).

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

Ожидаемые объёмы (оценочно): интернет-магазин среднего размера — сотни–первые тысячи отправлений/сутки; опрос трек-статусов — на порядок больше вызовов (несколько статусов на отправление за жизненный цикл). Уточняется под конкретный проект на этапе разработки.

Горячие пути: расчёт стоимости на checkout — единственный синхронный внешний вызов модуля, бюджет estimate_timeout_ms (дефолт 2000 мс, соответствует общему лимиту стандарта «≤2 с + graceful fallback») и явная политика фолбэка (estimate_fallback_policy); список ПВЗ — из кеша delivery-ru:pickup-points, 0 внешних вызовов на горячем пути.

Индексы: cms_delivery_ru_shipments — FK order_id, индекс status (пачки для опроса); cms_delivery_ru_pickup_points — составной provider_code, external_id; при фильтрации списка ПВЗ по гео-радиусу — пространственный/составной индекс под coordinates (уточняется при реализации под конкретную БД).

Внешние вызовы к службе доставки — только из очереди (создание отправления, синхронизация ПВЗ, опрос трек-статусов) — кроме синхронного расчёта стоимости на checkout, для которого явно зафиксированы таймаут и graceful fallback выше: это единственное разрешённое исключение из общего правила (стандарт §9/анти-паттерны), а не случайное отступление.

Тег delivery-ru:pickup-points — кеш справочника ПВЗ, инвалидируется событием DeliveryPickupPointsSynced.

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

  • Опрос трек-статусов идемпотентен — повторный опрос не создаёт дублирующее событие (идемпотентность по tracking_number + status + timestamp/id события провайдера: проверка текущего состояния перед записью новой строки журнала переходов, не слепой инсерт).
  • Печать этикетки недоступна без активного отправления (проверка состояния перед вызовом).
  • Вебхуки трек-статуса — подпись HMAC (секрет per-провайдер из .env) + проверка timestamp против replay (аналогично интеграциям).
  • Секреты доступа к API служб доставки — только .env.
  • Права: delivery-ru.view, delivery-ru.manage.

ПДн-паспорт: провайдеру (службе доставки) при создании отправления передаются ФИО, адрес и телефон получателя — минимально необходимый набор для доставки; модуль не хранит их отдельной копией сверх заказа (см. «Модель данных»). Согласие на передачу — часть согласия на обработку заказа (форма заказа ядра), отдельного согласия модуль не запрашивает.

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

Действиеадминменеджерstudio
Просмотр отправлений/ПВЗ (delivery-ru.view)
Печать этикетки (delivery-ru.manage)
Kill-switch способа доставки, лимиты, fallback-политика
Ручной запуск синхронизации ПВЗ вне расписания

Стоимость API-вызова (если провайдер тарифицирует по числу запросов) — видна в админке через delivery-ru.price_per_request, справочно, без автоматического биллинга ядра.

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

Админ:

  • Статус синхронизации: время последнего успешного обновления справочника ПВЗ, возраст кеша с предупреждением при превышении pickup_points_max_age_hours; отставание очереди опроса трек-статусов — на дашборде модуля.
  • Журнал обмена с провайдером (запросы/ответы) — отдельно от бизнес-лога, для разбора инцидентов.
  • Ошибки на человеческом языке: «СДЭК не отвечает, расчёт доставки временно недоступен» вместо технической ошибки/Error 500; «Boxberry недоступен — повторите печать позже».
  • Подтверждение при повторной печати этикетки для уже существующего label_url (предупреждение о риске дубля у оператора, не молчаливый повтор).

Покупатель:

  • Виджет карты ПВЗ при недоступности справочника — показывает последнюю закешированную версию без блокировки выбора; полностью пустой список — с формулировкой «нет пунктов выдачи в этом районе, выберите курьерскую доставку», не пустую карту без пояснения.
  • Расчёт доставки на checkout укладывается в воспринимаемый бюджет estimate_timeout_ms; при фолбэке пользователь видит понятную формулировку способа доставки («стоимость уточним после оформления» / фиксированная ставка), не индикатор ошибки.

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

  1. Расчёт стоимости недоступен (таймаут/ошибка провайдера) на checkout → политика estimate_fallback_policy: дефолт defer — способ доставки показывается с пометкой «стоимость уточним после оформления», заказ не блокируется; альтернативы fixed_rate (сумма fixed_rate_amount) и hide (скрыть способ) — включаются явно настройкой, не выбираются кодом наугад. Ответ API — 200 с meta.degraded: true (конвенция ядра, ревизия 14.07.2026), не 5xx.
  2. Трек-статус не входит в известный маппинг (новый/неизвестный статус от провайдера) → сохраняется как status = unknown с оригинальным кодом в логе для последующего маппинга; не роняет обработку вебхука и не блокирует показ остального трека покупателю.
  3. Справочник ПВЗ устарел (синхронизация упала) → используется закешированная версия (тег delivery-ru:pickup-points не инвалидируется без успешной синхронизации), админ видит предупреждение о возрасте кеша при превышении pickup_points_max_age_hours.
  4. Адрес не бьётся с ФИАС/для расчёта (некорректный/неполный адрес) → dadata (suggest-provider, если включён) нормализует адрес перед расчётом; при выключенном/недоступном dadata — расчёт по минимально введённым данным (город/индекс) либо явная ошибка пользователю с указанием уточнить адрес, не молчаливо неверный расчёт.
  5. Дубль вебхука трек-статуса → идемпотентность по tracking_number + status + timestamp (или provider-специфичному id события): повторный вебхук не создаёт повторное событие DeliveryStatusChanged и не пишет вторую строку журнала переходов (углубление формулировки из «Безопасность»).
  6. Недоступность провайдера при печати этикетки → понятная ошибка администратору («Boxberry временно недоступен, повторите позже»), повтор запроса не создаёт дублирующее отправление — печать идемпотентна относительно уже созданного shipment_id.
  7. Расхождение весогабаритов на этапе создания отправления vs расчёта на checkout → провайдер может пересчитать итоговую стоимость при создании отправления; расхождение с ранее показанной суммой логируется и алертится (cms/health/аудит), не проходит втихую — заказ не блокируется, менеджер видит расхождение для ручной сверки.
  8. Rate-limit провайдера при массовом опросе трек-статусов → батчинг пачками (tracking_poll_batch_size) и троттлинг между пачками; ответ 429 от провайдера — backoff, не параллельный шторм повторных запросов.
  9. Выключение/kill-switch провайдера доставки посреди checkout-сессии → способ, выбранный пользователем до выключения, при финализации заказа переоценивается; если способ более недоступен, checkout требует повторного выбора с понятным сообщением, не падает 500.
  10. Пустой список ПВЗ для адреса → виджет карты — состояние «нет пунктов выдачи поблизости», не пустая карта без пояснения (см. UX-требования).

Мини-ранбук (эксплуатация):

СимптомЧто проверитьКоманда
Расчёт доставки на checkout не отвечает / частый фолбэкдоступность провайдера, estimate_timeout_ms, circuit breakercms:delivery-ru:doctor --json
Справочник ПВЗ не обновляетсяпоследний успешный запуск синхронизации, доступность API провайдера, pickup_points_max_age_hourscms:delivery-ru:sync-pickup-points --json (ручной прогон)
Трек-статусы не обновляютсяотставание очереди delivery-ru, rate-limit провайдера, журнал вебхуковcms:delivery-ru:poll-tracking --json

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

Донор: — (новая разработка). Легаси-импорт (стандарт §16) не применим — исторических отправлений у донора нет. При появлении переноса из внешней системы логистики — тот же паттерн команды cms:delivery-ru:import-legacy --source=<профиль>, идемпотентной по external_id/tracking_number, с --dry-run.

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

  • [ ] Мок API службы доставки: тест расчёта стоимости, создания отправления, опроса трек-статуса
  • [ ] Опрос трек-статусов идемпотентен — повторный опрос не создаёт дублирующее событие
  • [ ] При недоступности провайдера checkout деградирует до оставшихся активных способов доставки
  • [ ] Таймаут расчёта стоимости на checkout не блокирует оформление заказа (graceful fallback)
  • [ ] Печать этикетки недоступна без активного отправления (проверка состояния)
  • [ ] Права delivery-ru.manage разграничивают просмотр отправлений и печать этикеток
  • [ ] Вебхук трек-статуса: подпись проверяется, невалидная подпись/протухший timestamp отклоняются
  • [ ] Дубль вебхука не создаёт повторное событие DeliveryStatusChanged
  • [ ] Контрактный тест деградации расчёта: недоступность провайдера → 200 + meta.degraded, политика estimate_fallback_policy отрабатывает согласно настройке
  • [ ] Неизвестный трек-статус не роняет обработку вебхука, попадает в unknown с логированием
  • [ ] Устаревание справочника ПВЗ отображается предупреждением в админке (pickup_points_max_age_hours)
  • [ ] Rate-limit провайдера (мок 429) при опросе трек-статусов не приводит к шторму запросов
  • [ ] Матрица ролей: delivery-ru.view/delivery-ru.manage проверены тестами доступа
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут; тестовая БД только delivery_ru_test, migrate:fresh запрещён

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