Описание функции
Хранить все обработанные providerEventId отгрузки, а не только последнее значение в ms3_shipments.last_event_id.
Проблема, которую решает
В v1 (#591, PR #605) идемпотентность webhook завязана на одно поле last_event_id. После событий A и B повтор A уже не replay. Перевозчики часто шлют старые callback повторно. Сейчас это может дать 409 или повторный transition вместо тихого 200.
Предлагаемое решение
Таблица вроде ms3_shipment_events (уникальность по shipment_id + provider_event_id). applyProviderEvent сначала ищет id в истории. При попадании возвращает текущую строку без записи. last_event_id можно оставить как кэш последнего id.
Сверяться с payment lifecycle (#590), если там появится такая же таблица событий.
Альтернативные варианты
Оставить last_event_id и требовать от провайдера монотонные id. Это ломает реальные CDEK/DPD retry.
Считать replay по (status, tracking_number, external_id). Ложные совпадения при легитимном повторном shipped с тем же треком.
Примеры использования
POST /api/v1/delivery/webhook/{delivery_id}
event id=evt-shipped → 200, shipment shipped
event id=evt-transit → 200, shipment in_transit
event id=evt-shipped (retry) → 200, без второй смены статуса и без 409
Дополнительный контекст
Описание функции
Хранить все обработанные
providerEventIdотгрузки, а не только последнее значение вms3_shipments.last_event_id.Проблема, которую решает
В v1 (#591, PR #605) идемпотентность webhook завязана на одно поле
last_event_id. После событий A и B повтор A уже не replay. Перевозчики часто шлют старые callback повторно. Сейчас это может дать 409 или повторныйtransitionвместо тихого 200.Предлагаемое решение
Таблица вроде
ms3_shipment_events(уникальность поshipment_id+provider_event_id).applyProviderEventсначала ищет id в истории. При попадании возвращает текущую строку без записи.last_event_idможно оставить как кэш последнего id.Сверяться с payment lifecycle (#590), если там появится такая же таблица событий.
Альтернативные варианты
Оставить
last_event_idи требовать от провайдера монотонные id. Это ломает реальные CDEK/DPD retry.Считать replay по
(status, tracking_number, external_id). Ложные совпадения при легитимном повторном shipped с тем же треком.Примеры использования
Дополнительный контекст
ShipmentLifecycleService::isReplay(), колонкаlast_event_idв20260819150000_create_shipments.php