Описание проблемы
После тега v1.13.0-beta1 (13 августа 2026, коммит f8858a3) в beta вошли lifecycle оплаты (#604), отгрузки (#605) и смена статуса заказа через порты (#596). На текущем HEAD f0dfe59 два сценария оставляют заказ и внешнее событие в разных состояниях. Учёт остатков в эту ветку не влит.
Шаги воспроизведения
Отгрузка:
- Включить
ms3_shipment_enabled.
- Создать отгрузку заказа, который ещё не в статусе «Отправлен».
- Принять webhook
shipped с provider_event_id. Параллельно сделать так, чтобы OrderStatusService::ensure() не смог поставить ms3_status_sent: плагин на msOnBeforeChangeOrderStatus возвращает ошибку, либо переход закрыт ms3_order_status_transitions.
- Проверить
msOrder.status_id и строку в ms3_shipments.
- Повторить тот же webhook с тем же
provider_event_id.
Оплата:
- Оставить
ms3_payment_on_failed_status по умолчанию (5, отмена).
- Принять webhook
failed по открытой попытке.
- Выпустить новую попытку на том же заказе и принять webhook
paid с другим external_id.
Ожидаемое поведение
Webhook shipped либо переводит заказ в ms3_status_sent вместе с отгрузкой, либо не занимает provider_event_id, чтобы повторный запрос дожал статус.
Повторная успешная оплата того же заказа после failed записывается в попытку. Если статус заказа уже сохранён, а упало только письмо или msOnChangeOrderStatus, webhook не отвечает 409.
Фактическое поведение
ShipmentLifecycleService::applyWithEventClaim пишет provider_event_id и статус отгрузки, делает commit и только потом вызывает syncOrderStatus. Ошибка синхронизации уходит в error.log и не меняет ответ webhook. Повтор с тем же provider_event_id попадает в ветку, где claimEvent возвращает false: транзакция откатывается, метод возвращает текущую отгрузку и не вызывает afterCommit. Заказ остаётся в прежнем статусе, перевозчик получает успех.
PdoPaymentAttemptStore использует $modx->pdo. У MODX этот handle в ERRMODE_SILENT: неудачный execute() возвращает false и не бросает исключение. PdoShipmentStore::executeWrite это учитывает и на SQLSTATE 23000 возвращает false. PdoPaymentAttemptStore::recordEvent и update результат execute() не проверяют. Если вставка события не прошла без исключения, recordEvent всё равно возвращает true, и writeWithEvent коммитит изменение попытки без строки в ms3_payment_attempt_events. Следующий webhook с тем же id не видит hasEvent и может применить сумму ещё раз. В юнит-тестах PDO обычно кидает исключения, поэтому этот путь там не виден.
PaymentLifecycleService::commit сначала коммитит попытку в writeWithEvent, затем вызывает syncOrderStatus. Если OrderStatusService::change() уже сохранил status_id, а msOnChangeOrderStatus вернул ошибку, метод отдаёт текст ошибки и не доходит до уведомлений. syncOrderStatus превращает любую строку в PaymentLifecycleException, webhook отвечает 409. Повтор идёт в ensure(): статус уже целевой, метод сразу возвращает true и уведомление снова не отправляет.
При настройке по умолчанию webhook failed или cancelled ставит заказу статус 5. assertPaidPreconditions отклоняет следующий paid, пока заказ отменён. Проверка стоит до commit, поэтому попытка не помечается оплаченной, а клиент платёжки получает 409.
Скриншоты
Нет. Расхождение видно по строкам ms3_shipments, ms3_shipment_events, ms3_payment_attempts и по status_id заказа.
Окружение
- MiniShop3: beta после v1.13.0-beta1, HEAD f0dfe59
- MODX: 3.x
- PHP: 8.2+
- MySQL: 8, PDO из MODX (
ERRMODE_SILENT)
- Браузер: не участвует, это webhook и сервисы
Логи ошибок
Логи из core/cache/logs/error.log
ShipmentLifecycleService: order #<id> status sync to <status_id> failed: <текст ошибки>
Дополнительный контекст
Приоритеты по находкам:
-
priority: high. Отгрузка и статус заказа расходятся, повтор webhook это не чинит. core/components/minishop3/src/Services/Shipment/ShipmentLifecycleService.php, applyWithEventClaim (около строк 464-504) и syncOrderStatus (около 339-366).
-
priority: high. Неуспешная оплата по умолчанию отменяет заказ, следующая успешная оплата того же заказа получает 409 и не пишется в попытку. PaymentLifecycleService::orderStatusFor и assertPaidPreconditions в core/components/minishop3/src/Services/Payment/PaymentLifecycleService.php.
-
priority: medium. PdoPaymentAttemptStore::recordEvent не смотрит на false от execute(). Тот же тихий PDO уже разобран в PdoShipmentStore::executeWrite.
-
priority: medium. Ошибка плагина msOnChangeOrderStatus после уже сохранённого статуса даёт 409 на оплате и срывает уведомление. Повтор через ensure() его не досылает. OrderStatusService::change (около строк 201-219) и PaymentLifecycleService::commit (около 315-324).
Проверял код на ветке beta в диапазоне v1.13.0-beta1..HEAD (88 коммитов). Ветку учёта остатков не смотрел: её нет в HEAD.
Описание проблемы
После тега v1.13.0-beta1 (13 августа 2026, коммит f8858a3) в beta вошли lifecycle оплаты (#604), отгрузки (#605) и смена статуса заказа через порты (#596). На текущем HEAD f0dfe59 два сценария оставляют заказ и внешнее событие в разных состояниях. Учёт остатков в эту ветку не влит.
Шаги воспроизведения
Отгрузка:
ms3_shipment_enabled.shippedсprovider_event_id. Параллельно сделать так, чтобыOrderStatusService::ensure()не смог поставитьms3_status_sent: плагин наmsOnBeforeChangeOrderStatusвозвращает ошибку, либо переход закрытms3_order_status_transitions.msOrder.status_idи строку вms3_shipments.provider_event_id.Оплата:
ms3_payment_on_failed_statusпо умолчанию (5, отмена).failedпо открытой попытке.paidс другимexternal_id.Ожидаемое поведение
Webhook
shippedлибо переводит заказ вms3_status_sentвместе с отгрузкой, либо не занимаетprovider_event_id, чтобы повторный запрос дожал статус.Повторная успешная оплата того же заказа после
failedзаписывается в попытку. Если статус заказа уже сохранён, а упало только письмо илиmsOnChangeOrderStatus, webhook не отвечает 409.Фактическое поведение
ShipmentLifecycleService::applyWithEventClaimпишетprovider_event_idи статус отгрузки, делает commit и только потом вызываетsyncOrderStatus. Ошибка синхронизации уходит в error.log и не меняет ответ webhook. Повтор с тем жеprovider_event_idпопадает в ветку, гдеclaimEventвозвращает false: транзакция откатывается, метод возвращает текущую отгрузку и не вызываетafterCommit. Заказ остаётся в прежнем статусе, перевозчик получает успех.PdoPaymentAttemptStoreиспользует$modx->pdo. У MODX этот handle вERRMODE_SILENT: неудачныйexecute()возвращает false и не бросает исключение.PdoShipmentStore::executeWriteэто учитывает и на SQLSTATE 23000 возвращает false.PdoPaymentAttemptStore::recordEventиupdateрезультатexecute()не проверяют. Если вставка события не прошла без исключения,recordEventвсё равно возвращает true, иwriteWithEventкоммитит изменение попытки без строки вms3_payment_attempt_events. Следующий webhook с тем же id не видитhasEventи может применить сумму ещё раз. В юнит-тестах PDO обычно кидает исключения, поэтому этот путь там не виден.PaymentLifecycleService::commitсначала коммитит попытку вwriteWithEvent, затем вызываетsyncOrderStatus. ЕслиOrderStatusService::change()уже сохранилstatus_id, аmsOnChangeOrderStatusвернул ошибку, метод отдаёт текст ошибки и не доходит до уведомлений.syncOrderStatusпревращает любую строку вPaymentLifecycleException, webhook отвечает 409. Повтор идёт вensure(): статус уже целевой, метод сразу возвращает true и уведомление снова не отправляет.При настройке по умолчанию webhook
failedилиcancelledставит заказу статус 5.assertPaidPreconditionsотклоняет следующийpaid, пока заказ отменён. Проверка стоит доcommit, поэтому попытка не помечается оплаченной, а клиент платёжки получает 409.Скриншоты
Нет. Расхождение видно по строкам
ms3_shipments,ms3_shipment_events,ms3_payment_attemptsи поstatus_idзаказа.Окружение
ERRMODE_SILENT)Логи ошибок
Логи из core/cache/logs/error.log
Дополнительный контекст
Приоритеты по находкам:
priority: high. Отгрузка и статус заказа расходятся, повтор webhook это не чинит.
core/components/minishop3/src/Services/Shipment/ShipmentLifecycleService.php,applyWithEventClaim(около строк 464-504) иsyncOrderStatus(около 339-366).priority: high. Неуспешная оплата по умолчанию отменяет заказ, следующая успешная оплата того же заказа получает 409 и не пишется в попытку.
PaymentLifecycleService::orderStatusForиassertPaidPreconditionsвcore/components/minishop3/src/Services/Payment/PaymentLifecycleService.php.priority: medium.
PdoPaymentAttemptStore::recordEventне смотрит на false отexecute(). Тот же тихий PDO уже разобран вPdoShipmentStore::executeWrite.priority: medium. Ошибка плагина
msOnChangeOrderStatusпосле уже сохранённого статуса даёт 409 на оплате и срывает уведомление. Повтор черезensure()его не досылает.OrderStatusService::change(около строк 201-219) иPaymentLifecycleService::commit(около 315-324).Проверял код на ветке beta в диапазоне v1.13.0-beta1..HEAD (88 коммитов). Ветку учёта остатков не смотрел: её нет в HEAD.