Skip to content

Core: introduce a shipment abstraction for fulfillment and tracking #591

Description

@Ibochkarev

Current implementation

msDelivery — справочник методов доставки (name, price, weight_price, class, properties, validation_rules, free_delivery_amount). Связь с оплатой: msDeliveryMember. На заказе хранится delivery_id + delivery_cost.

Контракт провайдера: DeliveryProviderInterface с единственным методом getCost(). База Delivery / stub DefaultDelivery считают стоимость (вес, порог бесплатной доставки, %/фикс). Пример в docblock — CDEK API только для тарифа.

Checkout: draft выбирает delivery_id через OrderFieldManager (валидация + validation_rules метода). Submit проверяет пару delivery/payment (DeliveryService::getDeliveryPaymentPairError) и required address fields. После submit менеджер может менять delivery_id через ManagerOrderMutationService::update.

Fulfillment: отдельной сущности нет. «Отправлен» = статус заказа ms3_order_status_sent (seed id 4, final=1). Dedicated setting вроде ms3_status_sent в seed map статусов нет (в отличие от new/paid/canceled). События — только общие msOnBeforeChangeOrderStatus / msOnChangeOrderStatus. Отдельного msOnShip* нет.

Трекинг: колонки tracking_number / таблицы shipment нет. В vueManager OrderTabsRegistry — пример ключа tracking для plugin tab (UI hook из #166), без модели данных в core. msOrder / msOrderAddress / msOrderProduct имеют JSON properties, куда пакеты могут писать ad-hoc.

DeliveryService (ms3_delivery_service): load controller, cost, payment pairing, remove. Нет createShipment / updateTracking / webhook.

Тесты: cost, delivery/payment pair, reference CRUD. Lifecycle отгрузки / tracking / async callback — нет.

Delivery vs Shipment distinction

Delivery method (msDelivery) Shipment (сейчас)
Смысл Как покупатель получает заказ Конкретная отгрузка заказа
В core Да Нет
Поля тарификация, class, validation
Состояние active справочник подмена через order.status_id = sent
Внешний id / трек нет

Core сейчас моделирует только Delivery method. Shipment как сущность отсутствует.

Problems

  1. Нет различия delivery method vs shipment: fulfillment = смена статуса заказа.
  2. Нет поля/модели для tracking number; плагины вынуждены класть данные в properties или свои таблицы.
  3. Статус отгрузки нельзя вести отдельно от order status (in transit / delivered / returned без плодения статусов заказа).
  4. DeliveryProviderInterface = только getCost. Нет контракта create/label/callback статуса доставки.
  5. Async обновления от CDEK / Почты / Яндекса / DPD в core некуда приземлять единообразно.
  6. Нет события «заказ отгружен» сверх общего status change; sent ещё и final — дальше по статусам заказа обычно нельзя.
  7. Partial / multi-shipment в схеме не предусмотрены (order lines без shipped qty). Это не блокер для v1, но ad-hoc properties усложнят эволюцию.

Вне scope v1: multi-warehouse, WMS, обязательные multi-parcel, реализация CDEK/DPD в core.

Связано: #568 (delivery/list discovery), #590 (payment lifecycle). Этот issue — fulfillment/shipment, не checkout list и не оплата.

Proposed Core abstraction

Минимально (имена на PR):

msShipment (1:1 с заказом в v1)
  id
  order_id
  delivery_id          # snapshot метода на момент отгрузки
  status               # preparing | shipped | in_transit | delivered | cancelled | returned | failed
  tracking_number
  external_id          # id у провайдера
  carrier / provider   # optional string / class key
  shipped_at
  delivered_at
  meta (json, non-secret)
  • ShipmentLifecycleService (DI): create (из order), transition status, setTracking, applyProviderEvent.
  • Смена order status (например в sent / custom) — policy через OrderStatusService, не прямой status_id.
  • DeliveryProviderInterface сохранить для cost. Опционально второй интерфейс (ShipmentProviderInterface: createShipment, handleWebhook) без ломки существующих cost-only классов.
  • v1: один active shipment на order. Partial/multi — follow-up (line allocations).

Shipment lifecycle

Рекомендуемый минимум:

(none) → preparing → shipped → in_transit → delivered
                         ↘ cancelled | failed
              delivered / shipped → returned (policy)

Маппинг на order status (configurable):

  • shipped (или in_transit) → order sent (если ещё не final-conflict)
  • delivered → optional custom status или только shipment status
  • cancelled / failed на shipment до sent → order cancel policy

Магазины без курьерских API: менеджер создаёт shipment вручную + вводит трек, либо только статус заказа как сейчас (setting ms3_shipment_enabled / soft mode).

Tracking

  • tracking_number — first-class field, обновляется независимо от order status.
  • События msOnBeforeUpdateShipmentTracking / msOnUpdateShipmentTracking (имена на PR).
  • Публичный cabinet/order DTO может отдавать tracking без secrets провайдера.

Partial shipment considerations

v1: целиком заказ = один shipment. Не требовать line-level shipped qty.

Зафиксировать в дизайне: будущий msShipmentItem (shipment_id, order_product_id, qty) не ломает 1:1 API (order.shipments[] длиной 1). Не реализовывать multi в этом issue.

External provider integration

Пакет (CDEK и т.д.):

  1. DeliveryProviderInterface для тарифа на checkout (как сейчас).
  2. Опционально shipment provider: создать накладную, сохранить external_id + tracking через LifecycleService.
  3. Webhook → core route / documented slot → verify → applyProviderEvent.
  4. Credentials в msDelivery.properties / settings, не в публичных DTO.

Core не реализует API перевозчиков.

Events

Помимо status order events:

  • before/after shipment create
  • before/after shipment status change
  • tracking update
  • (optional) provider callback applied

Плагины на msOnChangeOrderStatus остаются; для fulfillment предпочтителен shipment event, чтобы in_transit не требовал нового order status.

REST API

Backward compatibility

  • msDelivery, getCost, draft delivery_id, submit validation — без ломания.
  • Статус sent и ручная смена статуса менеджером работают как сейчас.
  • Пока shipment выключен / не создан: поведение идентично текущему core.
  • Cost-only delivery classes без shipment interface продолжают работать.
  • Plugin order tabs (tracking UI) могут перейти на core fields вместо private storage.

Tests

  • shipment creation from order (delivery_id snapshot)
  • shipment status update + optional order status via OrderStatusService
  • tracking update independent of order status
  • delivery completion (delivered)
  • cancellation / failed delivery policies
  • asynchronous provider callback (idempotent)
  • DefaultDelivery / cost-only provider unchanged
  • public DTO без msDelivery.properties secrets

Acceptance criteria

  • В core есть сущность shipment (или эквивалент), отдельная от msDelivery method и от одного только order.status_id.
  • Можно сохранить и обновить tracking_number без смены статуса заказа.
  • Есть LifecycleService (или аналог) со status transitions и хуками в OrderStatusService по policy.
  • Cost-only DeliveryProviderInterface не ломается; внешний пакет может обновлять shipment async через документированный extension point.
  • v1 не требует multi-shipment / partial lines; дизайн не закрывает follow-up.
  • При выключенном/неиспользуемом shipment checkout и статусы заказа регрессионно совпадают с текущим поведением.
  • Нет реализации CDEK/Почты/DPD/Яндекса в этом issue.

Дополнительный контекст

Аудит по коду beta без правок. Delivery method + cost extension уже есть. Не хватает shipment/tracking lifecycle для fulfillment-пакетов.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions