Тема
ТЗ — Доставка (cms/commerce-delivery)
Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке
Назначение и возможности
Абстракция доставки поверх заказа: контракт DeliveryProvider, методы доставки (курьер/ПВЗ/самовывоз), зоны и правила расчёта стоимости. Конкретные транспортные компании — коннекторы отдельным модулем, см. /cms-v2/integrations.
- Контракт
DeliveryProvider(calculate/createShipment/track/pickupPoints) - Методы доставки: курьер, пункт выдачи (ПВЗ), самовывоз со склада
- Зоны доставки и правила стоимости (по сумме заказа/весу/городу)
- Расчёт стоимости и сроков на checkout по выбранному методу и адресу
- Трек-номера отправлений с обновлением статуса от провайдера
- Сплит составного заказа на несколько отправлений при товарах с разных складов
- Интеграция стоимости доставки в rule-engine скидок (
cms/commerce-promo) для действия «бесплатная доставка»
Зависимости и выключение
requires: ядро, cms/commerce-model (заказы, warehouse_id позиций) · suggests:cms/commerce-promo · provides: delivery-provider (контракт реализуют коннекторы cms/delivery-ru)
Поведение при выключении: checkout деградирует до единственного способа получения — самовывоз без расчёта стоимости, оформление заказа продолжает работать без вкладки доставки.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
commerce_delivery_methods | id, code, title, type (courier|pickup_point|self_pickup), provider_code (nullable), lock_version | методы доставки, type — PHP Enum |
commerce_delivery_zones | id, city_id (nullable), region_pattern, method_id, priority, is_active, lock_version | зоны действия метода |
commerce_delivery_rules | id, zone_id, condition (json — сумма/вес), price, free_from_amount (nullable) | правила стоимости, condition — JSONB + GIN |
commerce_shipments | id, order_id, method_id, warehouse_id (nullable), tracking_number, status, provider_status_raw (nullable), address (json) | отправление заказа |
order_id/method_id/zone_id/warehouse_id — FK constrained()->index(); warehouse_id ссылается на первичный id commerce_warehouses (владелец — cms/commerce-stock, только чтение через сервис — см. обмен данными). price/free_from_amount — integer minor units + currency_code, float запрещён. commerce_delivery_methods/zones несут lock_version (optimistic lock, §4 стандарта) — конкурентная правка справочника двумя админами в Filament отдаёт 409, не тихую перезапись. priority на зоне разрешает пересекающиеся зоны одного метода (например «весь город» и «центр города» с более дешёвым тарифом) — при расчёте побеждает зона с наибольшей специфичностью/приоритетом, не первая найденная.
ПДн-паспорт (матрица v2.2): commerce_shipments.address (JSON) и tracking_number — персональные данные (152-ФЗ). Хранятся, пока жив заказ, плюс commerce-delivery.address_retention_days после доставки (плановая анонимизация джобой по расписанию: address схлопывается до {city, zone_id}, tracking_number не удаляется — сам по себе не идентифицирует человека вне связки с адресом). Хук ядра «забыть по запросу» обнуляет address немедленно по запросу субъекта; заказ и финансовые записи (домен commerce-model) при этом не трогаются — доставка не финансовый журнал и вправе анонимизироваться раньше. «Выгрузить всё по субъекту» — адрес и трек-номера всех отправлений владельца заказов.
Входные и выходные данные
Входы
| Источник | Данные/поля | Чем валидируется |
|---|---|---|
| Форма checkout (адрес/город) | city_id, region_pattern-совместимый адрес, method_id | FormRequest whitelist, city_id — из RequestContext/справочника городов, не свободный текст |
API POST /api/v1/checkout/delivery-calculate | method_id, city_id, address, cart_amount, weight (nullable) | FormRequest whitelist; cart_amount/weight — из сервиса корзины, не принимаются от клиента как истина без сверки |
API GET /api/v1/checkout/delivery-methods | city_id | whitelist, city_id берётся из RequestContext, не парсится из query вручную |
| Вебхук трек-статуса от провайдера | tracking_number, status_code, event_id | граница cms/webhooks-in (подпись, идемпотентность по event_id), затем FormRequest на структуру payload |
| Импорт зон/тарифов (Filament/CLI) | zone, method, condition, price построчно | cms:commerce-delivery:import-legacy — маппинг + валидация схемы, см. «Донорский код» |
Событие OrderPaid/оформление заказа (cms/commerce-model) | order_id, items[] (с warehouse_id), method_id, address | внутренний, из EventBus/сервис-вызова, не пользовательский вход |
Выходы
| Потребитель | Данные | Формат |
|---|---|---|
| Checkout-форма | ответ POST /delivery-calculate | {data: {price, eta_days, method_id}, meta: {degraded}} |
| Блок «Выбор метода доставки» | список доступных методов для адреса/города | рендер через сервис модуля, не запрос из шаблона |
Событие ShipmentCreated/ShipmentStatusChanged/ShipmentDelivered | факт по отправлению | подписчики (уведомления, кабинет клиента, аудит) |
cms/commerce-promo (FilterBus) | стоимость доставки для пересчёта корзины | значение в общем пайплайне пересчёта, не отдельный API |
| Filament (админ) | список отправлений, статус трека, справочники | таблицы/формы UI |
Импорт-отчёт import-legacy | сколько зон/правил прочитано/создано/обновлено/пропущено | скачиваемый отчёт, построчные ошибки |
Whitelist-принцип: параметр расчёта или импорта вне описанных «Входов» (незарегистрированный method_id, произвольное поле в condition импорта) — отвергается 422 с кодом unknown_field, не молчаливый игнор.
Настройки (группа commerce-delivery)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
commerce-delivery.default_method | string | self_pickup | нет | Метод доставки по умолчанию |
commerce-delivery.free_delivery_threshold | int | null | нет | Порог суммы заказа для бесплатной доставки (в minor units) |
commerce-delivery.tracking_update_interval_hours | int | 6 | нет | Периодичность опроса статуса у провайдера |
commerce-delivery.calculation_provider_enabled | bool | true | нет | Kill-switch: аварийное отключение внешнего провайдера расчёта (форс-fallback без выключения модуля) |
commerce-delivery.fallback_behavior | string enum (fallback_tariff|manual_quote) | manual_quote | нет | Поведение при недоступном провайдере или включённом kill-switch |
commerce-delivery.fallback_tariff_amount | int | null | нет | Сумма фиксированного тарифа при fallback_behavior=fallback_tariff (minor units) |
commerce-delivery.calc_provider_timeout_ms | int | 2000 | нет | Таймаут синхронного вызова провайдера расчёта на checkout (≤2 с по §9/§10 стандарта) |
commerce-delivery.price_recalc_tolerance | int | 0 | нет | Допустимое расхождение (minor units) между расчётом на витрине и на checkout без запроса подтверждения |
commerce-delivery.max_zones_per_method | int | 200 | нет | Лимит количества зон на метод доставки |
commerce-delivery.max_rules_per_zone | int | 20 | нет | Лимит правил стоимости на зону |
commerce-delivery.address_retention_days | int | 365 | нет | Срок хранения адреса доставки после ShipmentDelivered до плановой анонимизации |
commerce-delivery.calc_failure_alert_threshold | int (%) | 20 | нет | Порог доли неуспешных расчётов, после которого алертит cms/health |
Достижение max_zones_per_method/max_rules_per_zone — понятная ошибка в Filament («Достигнут лимит зон для метода, обратитесь к студии для увеличения»), не 500 и не тихое обрезание списка.
API
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| GET | /api/v1/checkout/delivery-methods | public | Доступные методы доставки для адреса/города |
| POST | /api/v1/checkout/delivery-calculate | public | Расчёт стоимости и сроков доставки |
| GET | /api/v1/orders/{id}/shipment | auth (владелец заказа) | Статус и трек-номер отправления(-ий) |
| POST | /api/v1/admin/delivery-zones | admin (commerce-delivery.manage) | Создание/правка зоны и правил стоимости |
| GET | /api/v1/admin/shipments | admin (commerce-delivery.view) | Список отправлений с фильтром по статусу (keyset-пагинация) |
| POST | /api/v1/admin/shipments/sync-tracking | admin (commerce-delivery.manage) | Массовая синхронизация треков (постановка в очередь) |
Ответ delivery-calculate — всегда 200; при недоступном провайдере или активном kill-switch — meta.degraded: true + meta.degraded_reason (ревизия ядра 14.07.2026), не 5xx. Адрес вне зоны действия всех методов — тоже 200 с data: [] (пустой список доступных методов, как пустая выдача поиска), не 404: это легитимный результат расчёта, а не ошибка запроса. Конфликт версии при правке зоны (два админа) — 409 с кодом version_conflict. Повторное оформление заказа с устаревшей ценой доставки — 409 с кодом price_changed и актуальной ценой в теле ответа (см. «Крайние случаи»). POST /admin/shipments/sync-tracking принимает Idempotency-Key — повторный запрос с тем же ключом возвращает статус уже идущей синхронизации, а не запускает вторую.
Компоненты
Блоки (BlockRegistry): выбор метода доставки на checkout, карта ПВЗ — карта подгружается лениво (не в критическом пути первой отрисовки), контейнер резервирует высоту (без CLS), список методов доступен с клавиатуры. Filament: справочник методов и зон доставки (с валидацией пересечения зон при сохранении — предупреждение, не жёсткий запрет, приоритет решает конфликт на расчёте), правила стоимости, список отправлений с треком и массовым действием «синхронизировать треки выбранных». Демо-сидер (матрица v2.2): демо-зоны («по городу», «самовывоз») и демо-методы для галереи блоков и playground без ручного ввода.
Команды: cms:commerce-delivery:sync-tracking --json, cms:commerce-delivery:repair-shipments --json (см. «Фоновая работа»), cms:commerce-delivery:import-legacy --source=<профиль> --dry-run --json (см. «Донорский код»).
События и обмен
| Событие | Когда | Payload |
|---|---|---|
ShipmentCreated | отправление создано у провайдера | shipment_id, order_id, method_id, warehouse_id |
ShipmentStatusChanged | обновился статус трека | shipment_id, status, tracking_number |
ShipmentDelivered | отправление доставлено | shipment_id, order_id |
Слушает: событие оформления заказа/OrderPaid из cms/commerce-model (создание отправлений, со сплитом по warehouse_id позиций). FilterBus: участвует в пересчёте корзины/заказа действием «бесплатная доставка» из cms/commerce-promo (порядок: скидки → бонусы → стоимость доставки → итог).
Таблица взаимодействий
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
cms/commerce-model (заказы) | 4 (requires, сервис-вызов) | out | Модуль запрашивает состав позиций заказа (включая warehouse_id) для формирования отправлений и подтверждения адреса/суммы |
delivery-provider (коннекторы cms/delivery-ru) | 3 (provides-контракт) | in | Резолв конкретной реализации DeliveryProvider через DI: расчёт, создание отправления, трек, список ПВЗ |
cms/commerce-promo | 2 (FilterBus) | in | Правило «бесплатная доставка» переопределяет/обнуляет стоимость, рассчитанную этим модулем, в общем пересчёте корзины |
RequestContext (ядро) | 4 (сервис-вызов, core-contracts) | out | Город/сайт/локаль резолвятся ядром до контроллера; модуль берёт их из контекста, не парсит URL/заголовки сам |
| Внешний провайдер транспортной компании | внешний → 5 (job после приёма) | in | Вебхук трек-статуса принимает cms/webhooks-in (подпись, идемпотентность по event_id), дальше — job в очередь commerce-delivery |
ShipmentCreated/StatusChanged/Delivered | 1 (событие) | out | Факты для подписчиков (уведомления, кабинет клиента, аудит) — модуль не знает, кто слушает |
Фоновая работа
Именованная очередь commerce-delivery: создание отправления у провайдера, опрос статуса трека с периодичностью tracking_update_interval_hours, применение вебхук-события трек-статуса. Все внешние вызовы к транспортной компании — только из очереди (кроме синхронного расчёта на checkout с таймаутом calc_provider_timeout_ms и graceful fallback), идемпотентно (повторный опрос/повторный вебхук с тем же статусом не создаёт дублирующее событие), ретраи с backoff при недоступности провайдера.
Эксплуатация (матрица v2.2, §15 стандарта). Метрики в Pulse: доля неуспешных расчётов (calc_failure_rate, алерт по calc_failure_alert_threshold), отставание опроса треков (tracking_poll_lag_hours — факт минус tracking_update_interval_hours), доля отправлений с незамапленным статусом провайдера (unmapped_status_rate).
Типовые инциденты:
| Симптом | Что проверить | Чем чинится |
|---|---|---|
| Массовые ошибки/таймауты расчёта на checkout | calc_failure_rate в Pulse, health-чек DeliveryProviderCheck | Включить kill-switch calculation_provider_enabled=false вручную, разобраться с провайдером |
Очередь commerce-delivery отстаёт по опросу треков | Глубина очереди/Horizon, tracking_poll_lag_hours | cms:commerce-delivery:sync-tracking --json форсированным прогоном |
| Часть отправлений не получает статус по вебхуку | Логи cms/webhooks-in, unmapped_status_rate | cms:commerce-delivery:repair-shipments --json — переопрос провайдера для «зависших» отправлений |
| Расхождение зон/тарифов после правки в Filament не видно на витрине | Теги кеша commerce-delivery:zones/methods, cms:cache:inspect ядра | Инвалидация происходит автоматически по afterSave(); ручной прогрев — повторный запрос после инвалидации |
| Дублирующиеся отправления по одному заказу после сбоя воркера | commerce_shipments на дубли по order_id+method_id+warehouse_id | cms:commerce-delivery:repair-shipments --json помечает дубли отменёнными, оставляет первое созданное |
cms:commerce-delivery:repair-shipments — идемпотентна, поддерживает --dry-run, допустима к запуску на живом сайте без даунтайма.
Бэкап/рестор: в бэкап попадают все таблицы модуля целиком (методы, зоны, правила, отправления — исходные данные, не производные проекции). После рестора пересоздания не требуется, кроме прогрева тегов кеша commerce-delivery:zones и commerce-delivery:methods (восстанавливается штатно первым запросом); трек-статусы могут быть устаревшими на момент снятия бэкапа — ближайший плановый цикл sync-tracking подтягивает актуальные без ручных действий.
Производительность и кеш
Ожидаемые объёмы: тысячи расчётов стоимости в день на checkout даже на среднем магазине (каждое открытие корзины/смена адреса — новый расчёт). Расчёт на checkout — горячий путь пользовательского сценария оформления заказа, обязан быть быстрым: бюджет ≤4 запросов к БД (метод + зона + правила + при необходимости остаток склада) плюс не более 1 синхронного внешнего HTTP-вызова к провайдеру, ограниченного calc_provider_timeout_ms (≤2 с) с graceful fallback по fallback_behavior (§9/§10 стандарта — не более одного синхронного внешнего вызова на горячем пути).
Индексы: составной GIN на commerce_delivery_rules(zone_id, condition) под резолв правила по условию; index() на commerce_shipments.order_id под выборку по заказу; (city_id, method_id, priority) на commerce_delivery_zones под резолв зоны по городу с учётом приоритета при пересечении.
Теги commerce-delivery:zones, commerce-delivery:methods. Инвалидация — правкой справочника зон/методов в Filament (afterSave()), событием SettingChanged для порога бесплатной доставки и лимитов. Расчёт стоимости на checkout — по актуальным правилам без кеша адреса покупателя (персональные данные не кешируются); от дублирующих параллельных вызовов при двойном сабмите защищает не кеш результата, а короткий лок дедупликации (TTL несколько секунд, ключ — хеш от method_id+city_id+ cart_amount, без хранения адреса) — см. «Крайние случаи».
Безопасность
Адрес доставки и трек-номер — персональные данные, доступны только владельцу заказа (auth) и администратору с правом commerce-delivery.view. Расчёт стоимости и создание зон — через FormRequest-whitelist, без прямого приёма произвольного JSON- условия без валидации схемы. Секреты интеграции с провайдером (API-ключ ЛК) — только .env. Разграничение прав: commerce-delivery.view, commerce-delivery.manage.
Матрица ролей:
| Роль | Расчёт/методы (public) | Свой трек (auth) | Список отправлений | Управление зонами/тарифами | Массовая синхр. треков | Удаление зоны | Kill-switch провайдера |
|---|---|---|---|---|---|---|---|
| посетитель/гость | ✅ | — | — | — | — | — | — |
| покупатель (владелец заказа) | ✅ | ✅ (только свой) | — | — | — | — | — |
| менеджер | ✅ | ✅ (любой заказ) | ✅ (view) | ❌ | ✅ (view) | ❌ | — |
| админ | ✅ | ✅ | ✅ | ✅ (manage) | ✅ | ✅ | — |
| studio | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (настройка calculation_provider_enabled) |
UX-требования
Покупатель (checkout):
- стоимость и срок доставки показываются рядом с выбором метода сразу после ввода адреса/города, без перехода на отдельный шаг;
- если расчёт не удался (провайдер недоступен и
fallback_behavior=manual_quote) — сообщение «Точную стоимость доставки уточнит менеджер после оформления» вместо технической ошибки; заказ при этом всё равно оформляется (метод помечается «цена уточняется»); - при ошибке валидации формы (например неполный адрес) введённые данные, включая уже выбранный метод доставки, сохраняются — повторный ввод адреса с нуля не требуется;
- изменение стоимости между расчётом на витрине и оформлением заказа показывается явным диалогом с новой ценой и кнопкой подтверждения, а не тихой заменой суммы.
Админ:
- пустое состояние списка отправлений («Отправлений пока нет — появятся после первого оплаченного заказа с доставкой»), пустое состояние справочника зон («Зон пока нет, добавьте первую или импортируйте из легаси-сайта»);
- массовая синхронизация треков выбранных отправлений одним действием из списка;
- человеческая ошибка при конфликте зон: сохранение пересекающейся зоны не блокируется жёстко, но предупреждает «Зона пересекается с «Центр города» — приоритет определит, какая сработает» вместо тихого молчаливого сохранения.
Крайние случаи и типовые баги
- Стоимость доставки изменилась между расчётом на витрине и оформлением заказа → финальная стоимость пересчитывается на сервере в момент оформления; расхождение больше
price_recalc_tolerance→ заказ не оформляется по устаревшей цене без подтверждения, ответ409 price_changedс актуальной ценой, фронт показывает диалог подтверждения (см. UX выше). - Провайдер расчёта стоимости недоступен → поведение определяется настройкой
commerce-delivery.fallback_behavior, а не хардкодом:fallback_tariff— покупатель видит фиксированную стоимостьfallback_tariff_amount;manual_quote— сообщение «менеджер уточнит стоимость», заказ оформляется без вкладки точной цены. Kill-switchcalculation_provider_enabled=falseфорсирует тот же fallback без выключения модуля. - Составной заказ с товарами с разных складов → позиции заказа несут
warehouse_id(cms/commerce-model), модуль группирует их по складу и создаёт отдельное отправление (commerce_shipments.warehouse_id) на каждую группу;ShipmentCreatedиздаётся по каждому отправлению отдельно, не одно на заказ. - Зоны доставки по городу сверяются с
RequestContext→city_idберётся из контекста, резолвленного ядром (middleware), не парсится модулем из query/URL самостоятельно; смена города в контексте меняет доступные зоны на следующем запросе. - Адрес вне зоны действия ни одного метода → не ошибка ввода:
GET /delivery-methodsиPOST /delivery-calculateотвечают200с пустымdata, checkout показывает «Доставка по этому адресу недоступна, свяжитесь с нами» — не404/422(симметрично пустой выдаче поиска). - Трек-статус от провайдера пришёл с неизвестным кодом (маппинг не полный) →
provider_status_rawсохраняется всегда, внутреннийstatusвыставляется в безопасное значение «уточняется» (не падает, не молчит);unmapped_status_rateрастёт и алертит черезcms/health, что сигнализирует о необходимости расширить маппинг статусов провайдера. - Двойной сабмит формы расчёта → короткий лок дедупликации (TTL несколько секунд, ключ — хеш параметров расчёта без адреса) не даёт улететь второму синхронному вызову к провайдеру, пока первый в работе; клиент получает один и тот же результат.
- Удаление зоны, на которую ссылаются активные заказы в пути → soft-поведение:
is_active=falseвместо физического удаления; существующиеcommerce_shipmentsпродолжают жить и получать обновления трека, новые расчёты эту зону не видят. Жёсткое удаление доступно только когда у зоны нет отправлений в незавершённых статусах (проверка передDELETE, иначе409). - Пересекающиеся зоны одного метода в одном городе → расчёт использует зону с наивысшим
priority(при равенстве — более специфичныйregion_pattern), не первую найденную в БД; Filament предупреждает о пересечении при сохранении, но не запрещает — оверлеп с приоритетами может быть намеренным бизнес-решением. - Конкурентная правка одной зоны/метода двумя админами →
lock_version(optimistic lock, §4 стандарта): второйPUTбез актуальной версии получает409 version_conflict, не молчаливую перезапись. - ⚠️ Противоречие: кеш горячего пути расчёта vs запрет кеша ПДн-адреса. Highload-требования каталога (
commerce-model.md) толкают к кешированию горячих путей; правило безопасности §11 стандарта запрещает кешировать персональные данные, а адрес покупателя — ПДн. Разрешение: кешируются только неперсональные компоненты расчёта — справочники зон/методов/правил (тегиcommerce-delivery:zones/methods), сам ответ по конкретному адресу не кешируется и не переиспользуется между разными запросами; производительность горячего пути обеспечивается не кешем ответа, а малым числом индексированных запросов (бюджет ≤4) и локом дедупликации от двойного сабмита, а не хранением адреса.
Донорский код
Донор: — (новая разработка). Для переезда клиентов со старых сайтов (легаси-платформы без модульной CMS) — команда cms:commerce-delivery:import-legacy --source=<профиль> (матрица v2.2, §16 стандарта): маппинг старых таблиц зон/тарифов на новую схему (commerce_delivery_zones/rules), идемпотентна (повторный прогон обновляет по ключу external_id, не дублирует), поддерживает --dry-run с отчётом расхождений и скачиваемыми построчными ошибками. Прогон на копии донорских данных — часть приёмки для каждого клиентского переезда, где у клиента уже были настроены зоны/тарифы.
Тесты и приёмка
- [ ] Контрактный тест:
DeliveryProvider::calculate()возвращает стоимость и срок без создания отправления - [ ] Правило «бесплатная доставка от суммы» корректно применяется на checkout и в rule-engine скидок
- [ ] Смена статуса трека идемпотентна (повторный опрос/повторный вебхук с тем же статусом не дублирует событие)
- [ ] При выключении модуля checkout работает с единственным методом «самовывоз» без ошибок
- [ ] Зона без подходящего правила стоимости не позволяет оформить заказ с этим методом (валидация)
- [ ] Права
commerce-delivery.manage/.viewразграничивают справочники/зоны, отправления и массовую синхронизацию треков (матрица ролей) - [ ] Финальный пересчёт цены на оформлении заказа: расхождение сверх
price_recalc_toleranceдаёт409 price_changed, не тихое списание по старой цене - [ ] Kill-switch
calculation_provider_enabled=falseи оба режимаfallback_behaviorпокрыты тестом (fallback-тариф / «менеджер уточнит») - [ ] Составной заказ с позициями на разных складах создаёт отдельные отправления по
warehouse_id - [ ] Резолв зоны по
RequestContext(не по самостоятельному парсингу города модулем) - [ ] Адрес вне всех зон возвращает
200с пустым списком методов, не404/500 - [ ] Незамапленный статус провайдера не роняет обработку вебхука/опроса,
provider_status_rawсохранён - [ ] Двойной сабмит формы расчёта не создаёт два параллельных вызова провайдера (лок дедупликации)
- [ ] Удаление зоны с активными отправлениями в пути — soft (
is_active=false), жёсткое удаление блокируется при незавершённых отправлениях - [ ] Конкурентная правка зоны/метода двумя админами даёт
409 version_conflict, не «последний победил» - [ ]
cms:commerce-delivery:repair-shipments --dry-runнаходит дубли отправлений без их изменения - [ ]
cms:commerce-delivery:import-legacy --dry-runстроит отчёт расхождений на копии донорских данных, повторный прогон идемпотентен - [ ] «Забыть по запросу» обнуляет
addressотправлений субъекта, финансовые данные заказа не затрагивает - [ ] Контрактный набор
cms-testingзелёный, пакет тестируется в testbench-изоляции - [ ] Feature-тест на каждый роут API, провайдер доставки в тестах замокан
- [ ] Тестовая БД — только
commerce-delivery_test;migrate:fresh/refresh/resetзапрещены