К нам поступила заявка: часть покупателей не могла завершить заказ
К нам обратился владелец интернет-магазина на WordPress и WooCommerce. Покупатели жаловались, что после заполнения формы и нажатия кнопки подтверждения появляется ошибка. Товары при этом добавлялись в корзину, стоимость рассчитывалась, а страницы магазина открывались без заметных сбоев.
Сам владелец уже проверял оформление: добавил товар, выбрал курьерскую доставку и успешно создал заказ. Внешний мониторинг также не сообщал о недоступности сайта. Получалось, что магазин работал, но часть покупателей всё равно не могла завершить покупку.
Из разговора выяснилось, что незадолго до первых жалоб обновлялся модуль доставки. Мы зафиксировали это изменение как возможную зацепку. Чтобы связать обновление с неисправностью, требовалось воспроизвести ошибку и установить, какой код останавливает оформление.
Сначала проверили, что оставалось после неудачной попытки
Мы начали со сверки обращений покупателей с заказами WooCommerce и доступными журналами платёжного шлюза. Проверили в том числе заказы, ожидающие оплаты, неудачные и отменённые операции.
В тех попытках, которые удалось сопоставить по времени и условиям, не нашли соответствующих заказов и подтверждённых платежей. Это сузило поиск. На каком этапе обрывалось оформление, мы пока не знали: точку остановки предстояло установить при воспроизведении ошибки.
Перед изменениями сохранили актуальные копии файлов и базы, настройки расширений и доступные журналы. Рабочий магазин на этапе первичной диагностики не останавливали, настройки оплаты не меняли.
Воспроизвели ошибку при выборе пункта выдачи
Для диагностики подготовили отдельную копию магазина с теми же версиями WordPress, WooCommerce, темы и расширений. На ней исключили реальные списания, отправку писем покупателям, регистрацию настоящих отправлений и передачу изменений во внешние системы учёта.
Сначала повторили действия владельца. Использовали тот же сценарий: товар в корзине, заполненные контактные данные, курьерская доставка и тестовый способ оплаты. Заказ создался без ошибки.
Затем изменили только способ получения товара. Выбрали доставку в пункт выдачи, указали отделение и подтвердили оформление. На этот раз страница показала ошибку, а нового заказа в магазине не появилось.
Мы повторили проверку с другим товаром и другим отделением. Сбой сохранился. После этого сравнили гостевое оформление и покупку из зарегистрированной учётной записи: доставка в пункт выдачи не работала в обоих случаях. При возвращении к курьерской доставке заказ снова создавался.
Так мы установили условие, при котором возникала неисправность. Она зависела от способа доставки. Владелец не замечал её, потому что при своей проверке выбирал курьера.
Проследили запрос оформления до ошибки PHP
В магазине использовалось классическое оформление WooCommerce. Мы сравнили запросы браузера при успешной и неуспешной покупке. В обоих случаях форма отправляла данные обработчику ?wc-ajax=checkout, но при выборе пункта выдачи сервер возвращал HTTP 500.
Запрос доходил до сервера. Контактные данные, состав корзины и тестовый способ оплаты оставались прежними; различались данные доставки. В консоли браузера не обнаружили JavaScript-ошибок, которые объясняли бы наблюдаемый сбой. Следующий этап проверки перенесли в серверные журналы.

За время воспроизведённого запроса в журнале PHP появилась запись:
По стеку вызовов мы вышли на пользовательскую функцию в дочерней теме. Она отвечала за сохранение выбранного пункта выдачи и передавала в trim() значение, которое оказалось массивом вместо ожидаемой строки.
Обработчик выполнялся на этапе woocommerce_checkout_create_order. В исследованном сценарии нового заказа ошибка возникала перед его штатным сохранением. До последующего вызова обработки оплаты созданного заказа эта попытка не доходила. Такая последовательность согласовывалась с тем, что мы наблюдали в браузере и при проверке заказов.
Теперь у нас была конкретная точка сбоя. Оставалось выяснить, почему прежний обработчик начал получать данные другого типа.
Нашли несовместимость между обновлённым модулем и пользовательской доработкой
Мы сопоставили код обработчика с содержимым запроса и сравнили прежнюю версию интеграции с обновлённой.
Раньше выбранный пункт выдачи передавался одним строковым значением. Пользовательская функция получала идентификатор отделения, убирала пробелы по краям и сохраняла его в заказе.
После обновления в запросе появились вложенные поля pickup_point[id] и pickup_point[name]. На стороне PHP они формировали массив pickup_point: отдельно идентификатор отделения и отдельно его название.
Пользовательский код продолжал читать pickup_point целиком и передавать его строковой функции. Изменение формата подтвердилось при сравнении версий. Для курьерской доставки эта ветка обработки не выполнялась, поэтому соответствующие заказы проходили нормально.

Контрольной проверкой подтвердили роль обработчика
На копии магазина мы временно отключили только найденный пользовательский обработчик. Модуль доставки и остальные компоненты оставили включёнными. После этого повторили покупку с получением в пункте выдачи.
Заказ создался, а прежняя ошибка PHP исчезла. Однако в данных, которые использовала интеграция доставки, не оказалось необходимого идентификатора отделения. Именно его запись обеспечивала отключённая функция.
Мы вернули обработчик и исправили его работу. Другие пользовательские функции, связанные с пунктом выдачи, также проверили на использование прежнего формата. Это позволило охватить последующую подготовку отправления и вывод данных заказа, а не только место, в котором возникал TypeError.
Исправили подготовку данных пункта выдачи
Старый обработчик обращался непосредственно к входящему запросу уже при создании заказа. Мы перенесли подготовку значения на более ранний этап и подключили её через woocommerce_checkout_posted_data.
В обработчике этого фильтра наша функция отдельно читала дополнительное поле из запроса, проверяла его структуру и включала подготовленный идентификатор в данные оформления. Проверка принадлежности отделения выбранной доставке выполнялась следующим этапом.
Для нового формата обработчик извлекал значение id из массива pickup_point. Тип самого идентификатора проверялся до строковой обработки: вложенный массив вместо id больше не попадал в trim(). Для поддерживаемого прежнего строкового формата оставили отдельную ветку обработки. Неожиданную структуру не пытались принудительно преобразовать в строку.
После этого проверка и сохранение работали с одним подготовленным значением. Повторное чтение необработанного pickup_point из запроса в функции создания заказа убрали.
Проверили отделение до сохранения заказа
Исправление типа данных устраняло аварийную остановку, но ещё не гарантировало правильную доставку. В запросе мог отсутствовать идентификатор отделения или оставаться значение от предыдущего выбора покупателя.
Мы добавили проверку через woocommerce_after_checkout_validation. К этому этапу классического оформления WooCommerce уже обновлял данные сессии и рассчитывал итоговую стоимость. Наш обработчик сопоставлял текущий выбор доставки с доступным тарифом соответствующего пакета и проверял, требовался ли для него пункт выдачи.
Старое значение из сессии не использовали как замену актуальному выбору покупателя. Если адрес или способ доставки менялся, проверка учитывала состояние после пересчёта.
Для доставки в пункт выдачи мы проверяли наличие идентификатора, его формат и соответствие выбранной службе и региону. Сведения сверяли с данными действующей интеграции. Одного названия отделения, присланного браузером, для подтверждения выбора было недостаточно.

При некорректных данных обработчик добавлял сообщение в стандартную проверку WooCommerce. Оформление останавливалось до создания нового заказа, а покупатель получал просьбу выбрать доступное отделение. Аварийного завершения PHP в этой ситуации больше не было.
Отдельно учли переключение на курьера. После такой смены отделение переставало быть обязательным, а его прежнее значение не использовалось при сохранении данных доставки.
Восстановили сохранение сведений, необходимых модулю доставки
В обработчике создания заказа мы оставили запись уже подготовленного и прошедшего проверку идентификатора. Сохраняли его через объект WC_Order и метод update_meta_data(). Затем WooCommerce выполнял штатное сохранение заказа вместе с его метаданными.
Прямую запись в таблицы базы не использовали. Работа через объект заказа позволила не привязывать исправление к таблицам старого способа хранения или HPOS.
Мы также сверили имя метаполя, формат значения и место, из которого действующая интеграция забирала сведения для подготовки отправления. Так сохранили соответствие между исправленной доработкой и модулем доставки.
После изменения кода выбранное отделение сохранялось в заказе и было доступно при его дальнейшей обработке. Исправление осталось в пользовательской доработке. Файлы ядра WordPress, WooCommerce и установленного модуля доставки менять не потребовалось.
Проверили исправление на копии магазина
Мы начали с двух исходных сценариев. Курьерская доставка продолжила работать. При получении в пункте выдачи заказ теперь тоже создавался, а сохранённый идентификатор соответствовал выбранному отделению.
Затем поменяли отделение перед подтверждением покупки и проверили результат в заказе. Сохранилось последнее выбранное значение. Переключение с пункта выдачи на курьера и обратно также прошло корректно: стоимость пересчитывалась, а неактуальные сведения об отделении не переносились в заказ.
Оформление повторили без авторизации, из зарегистрированной учётной записи и на смартфоне. Проверили выбор отделения, обновление доставки и отправку формы.
После этого вернулись к ошибочным данным. Без обязательного отделения и с недопустимым значением идентификатора оформление останавливалось с понятным сообщением. Новые заказы с неполными сведениями не создавались. Прежний TypeError не повторялся.
В успешно созданных заказах мы сверили товары, количество, стоимость доставки, скидки и итоговую сумму. Проверили, что интеграция использовала сохранённый идентификатор при подготовке данных отправления. Настоящие отправления при этом не регистрировали.
Платёжную часть проверили отдельно в тестовом режиме: успешный результат, отказ и соответствующее изменение статуса заказа. В проверенных сценариях лишние заказы не появлялись. Также проверили формирование уведомлений покупателю и администратору, исключив отправку тестовых писем реальным клиентам.
После этих проверок исправление было готово к переносу.
Перенесли исправление на рабочий магазин
Откатывать всю базу не потребовалось. Пока шла диагностика, магазин продолжал принимать заказы с работающим способом доставки. Замена базы старой копией могла затронуть новые покупки, платежи, возвраты и остатки.
На время внедрения проблемный способ доставки сделали недоступным. Курьерская доставка продолжала работать. Перед заменой сохранили текущую версию изменяемых файлов и подготовили возврат предыдущей версии кода на случай неудачного выпуска.
На рабочий сайт перенесли только исправленную доработку. Базу и тестовые заказы с копии не переносили. После установки проверили работу обновлённого обработчика и исключения из кеширования для корзины, оформления заказа и персонализированных ответов. Затем снова включили доставку в пункт выдачи.
Подтвердили результат на рабочем сайте
Мы повторили оформление с тем способом доставки, который раньше приводил к ошибке. Для контрольной покупки использовали согласованный с владельцем вариант без реального списания денег и регистрации отправления.
Заказ появился в WooCommerce. В административной панели проверили выбранное отделение, стоимость доставки и итоговую сумму. Данные соответствовали контрольной покупке.

Контрольную покупку обработали отдельно от реальных заказов и проверили, что она не ушла в выполнение и не оставила искажений в учёте остатков.
После повторного оформления сопоставили ответы сервера и записи PHP-журнала. Проверенные запросы завершались штатно, прежняя ошибка с передачей массива в trim() больше не возникала.
Что изменилось для владельца магазина
Покупатели снова могли оформить заказ с доставкой в пункт выдачи. В заказе сохранялось выбранное отделение, а интеграция получала сведения, необходимые для подготовки отправления. Курьерская доставка продолжила работать.
Ключевой находкой было различие между двумя сценариями покупки. Успешный заказ с курьером скрывал неисправность доставки в пункт выдачи. Когда мы воспроизвели именно этот вариант, запрос оформления привёл к ошибке PHP, а стек вызовов — к конкретной доработке в дочерней теме.
После восстановления оставили контроль ошибок оформления. Для этого магазина проверка доступности страниц дополнилась наблюдением за операцией, на которой он терял покупателей, — созданием заказа.
