Заказ изнутри - коллекции, пересчёт, сохранение, события
Разбираем устройство заказа: из каких коллекций он собран, когда пересчитываются суммы, что возвращает сохранение и в каком порядке срабатывают события продаж.
Механика
Заказ - это дерево связанных объектов, а не одна строка таблицы. У него есть корзина с позициями, коллекция оплат, коллекция отгрузок и коллекция свойств, и каждая живёт своей жизнью.
Изменения копятся в объектах заказа и уходят в базу одним сохранением. Пока сохранение не вызвано, в базе лежит прежнее состояние заказа, и любая внешняя выборка видит именно его.
Суммы заказа пересчитываются не после каждой правки отдельного поля. Пересчёт запускается явно либо в момент сохранения, и это объясняет знаменитое «сумма в объекте одна, а в базе другая».
У позиции корзины может стоять признак своей ручной цены. Такая строка выпадает из пересчёта целиком: ни скидки каталога, ни правила корзины к ней больше не применяются.
Оплата и отгрузка - это отдельные документы внутри самого объекта заказа. Признак оплаты заказа складывается из оплат, а признак отгрузки - из отгрузок, и напрямую эти поля не правят.
Статус заказа сам по себе никак не связан с оплатой и отгрузкой. Он описывает движение заказа по процессу магазина, и связь со свойствами оплаты настраивают своим кодом или бизнес-правилами.
Сохранение заказа возвращает объект результата, а не простой признак успеха. В нём лежат ошибки всех уровней дерева, и без их чтения заказ молча остаётся несохранённым.
События модуля продаж срабатывают вокруг сохранения заказа в понятном фиксированном порядке. Сначала события до записи, где ещё можно вмешаться, затем сама запись, затем события после неё.
Отдельная сущность внутри заказа - это его свойства покупателя и доставки. Они живут своей коллекцией, привязаны к типу плательщика и читаются по коду свойства, а не по его названию.
Понимание этого дерева экономит больше всего времени при разборе жалоб. Вопрос «почему сумма не та» превращается в понятную последовательность проверок от позиции корзины до итога заказа.
Шаги
- Загрузить заказ и понять, какие именно коллекции понадобятся для нужной задачи.
- Менять данные в объектах коллекций, а не запросами в таблицы продаж.
- Явно запускать пересчёт, если суммы должны обновиться до сохранения заказа.
- Читать объект результата сохранения и показывать ошибки, а не глотать их.
- Вешать свою логику на события до записи, если нужно вмешаться в заказ.
Код
Загружаем заказ и обходим коллекции:
\Bitrix\Main\Loader::includeModule('sale');$order = \Bitrix\Sale\Order::load($orderId);printf("сумма=%s оплачен=%s статус=%s\n", $order->getPrice(), $order->isPaid() ? 'да' : 'нет', $order->getField('STATUS_ID'));foreach ($order->getBasket() as $item) { printf(" %s x%s\n", $item->getField('NAME'), $item->getQuantity()); }foreach ($order->getPaymentCollection() as $payment) { printf(" оплата %s\n", $payment->getSum()); }foreach ($order->getShipmentCollection() as $shipment) { if ($shipment->isSystem()) { continue; } // системную отгрузку пропускаем printf(" отгрузка %s\n", $shipment->getDeliveryName());}Каждая коллекция отдаёт свои объекты и знает своего родителя. Из позиции корзины можно подняться к заказу, а из заказа спуститься к любой оплате или отгрузке, не собирая ничего запросами.
Меняем состав и пересчитываем:
$basket = $order->getBasket();$item = $basket->getItemById($basketItemId);$item->setField('QUANTITY', 3);$order->setBasket($basket); // возвращаем корзину заказу$result = $order->save(); // пересчёт и запись идут вместеПересчёт при сохранении и объясняет частую ошибку. Код читает сумму сразу после правки количества, видит старое число и делает вывод, что платформа считает неправильно.
Читаем результат сохранения:
if (!$result->isSuccess()) { foreach ($result->getErrors() as $error) { printf("[%s] %s\n", $error->getCode(), $error->getMessage()); }}// ошибки приходят от всех уровней дерева: корзины, оплат, отгрузок, свойств// код ошибки удобно писать в журнал: по нему потом ищут причинуДобавляем оплату к заказу:
$payments = $order->getPaymentCollection();$payment = $payments->createItem(\Bitrix\Sale\PaySystem\Manager::getObjectById($paySystemId));$payment->setField('SUM', $order->getPrice());$payment->setField('CURRENCY', $order->getCurrency());$payment->setPaid('Y'); // отмечаем оплату оплаченной$order->save();// признак оплаты заказа появится сам, когда оплата будет отмечена оплаченнойСмотрим порядок событий сохранения:
$manager = \Bitrix\Main\EventManager::getInstance();$manager->addEventHandler('sale', 'OnSaleOrderBeforeSaved', static fn() => log('до записи'));$manager->addEventHandler('sale', 'OnSaleOrderSaved', static fn() => log('после записи'));$manager->addEventHandler('sale', 'OnSaleOrderPaid', static fn() => log('оплата отмечена'));// событие до записи умеет отменить сохранение, событие после - уже нетПроверяем, что заказ действительно записан:
$fresh = \Bitrix\Sale\Internals\OrderTable::getRow(['filter' => ['=ID' => $orderId], 'select' => ['ID', 'PRICE', 'PAYED', 'STATUS_ID', 'DATE_UPDATE']]);print_r($fresh); // сверяем состояние в базе с состоянием объекта в кодеprintf("в объекте: %s\n", $order->getPrice()); // числа должны совпастьПолезная привычка при разборе - печатать состояние заказа до и после сохранения. Две строки в журнале показывают, что именно изменилось, и снимают половину вопросов к платформе.
Ограничения
Прямые запросы в таблицы модуля продаж ломают согласованность его данных. Сумма заказа, признаки оплаты и резервы считаются кодом модуля, и правка строк мимо него оставляет заказ в противоречивом состоянии.
Позиция корзины со своей ручной ценой не участвует в пересчёте вовсе. Акции и купоны на неё не действуют до тех пор, пока признак ручной цены не снят явно.
Заказ, который уже уехал в учётную систему, менять на сайте рискованно. Правки на двух сторонах спорят между собой, и правило «чья версия главнее» согласуют заранее.
Тяжёлая работа в событиях сохранения замедляет оформление. Обращения к внешним сервисам и рассылку писем выносят в фоновое выполнение, а не делают в обработчике.
Отмена заказа не возвращает резервы и оплаты сама по себе. Эти шаги выполняют явно, иначе товар остаётся зарезервированным, а деньги - не возвращёнными.
Загрузка заказа целиком стоит нескольких запросов к базе данных. В списках и в отчётах поэтому берут табличную выборку, а объект заказа собирают только там, где его действительно меняют.
Типичные проблемы
Сумма заказа в коде не совпадает с суммой в базе.
Пересчёт не выполнялся, а изменения ещё не сохранены в базу данных. Суммы читают после сохранения либо после явного запуска пересчёта заказа.
Заказ не сохранился, а код не показал ни одной ошибки.
Результат сохранения не проверялся, а ошибки лежат именно в нём. Объект результата читают всегда и показывают его сообщения человеку или в журнал.
Скидка не применилась к позиции заказа.
У позиции стоит признак своей цены, и она выпала из пересчёта скидок. Признак ручной цены снимают, если позиция должна участвовать в акциях.
Признак оплаты не выставляется при добавлении оплаты.
Оплата создана, но не отмечена оплаченной, и заказ считает её ожидаемой. Признак оплаты заказа складывается из отмеченных оплат, а не из их наличия.
Оформление заказа стало заметно медленным.
В обработчике события сохранения выполняется медленный запрос к какому-то внешнему сервису. Такие медленные вызовы выносят в фоновое выполнение уже после ответа покупателю.
После правки заказа запросом сломались отчёты.
Строки таблиц продаж менялись напрямую, мимо кода модуля и его пересчётов. Заказ правят только через его объекты, даже если запрос кажется проще.
Частые вопросы
Почему сумма не меняется сразу после правки?
Пересчёт выполняется явно или при сохранении, а не после каждой правки поля. Это осознанное решение: пересчёт на каждое изменение был бы слишком дорогим.
Что возвращает сохранение заказа?
Объект результата с признаком успеха и списком ошибок всех уровней дерева. Без его проверки ошибки остаются незамеченными, а заказ - несохранённым.
Можно ли править заказ запросами к базе?
Технически можно, но так ломаются пересчёты, резервы и отчёты. Заказ меняют через его объекты, даже если это выглядит длиннее.
Чем статус отличается от признака оплаты?
Статус описывает движение заказа по процессу магазина и меняется людьми или кодом. Признак оплаты складывается из документов оплаты и меняется вместе с ними.
Где вешать свою проверку заказа?
На событие до записи: там ещё можно отменить сохранение с понятной ошибкой. События после записи годятся для уведомлений и для фоновой работы.
Смежное
- Заказы магазина - оглавление подтемы
- Заказ из кода: чтение, поиск по номеру, изменение состава - типовые операции с заказом
- Оплаты и отгрузки заказа: объекты, признаки, частичные операции - коллекции оплат и отгрузок подробно
- Заказ из кода не сохраняется: разбор причин - разбор ошибок сохранения
- События оформления заказа: свои проверки, запрет и допданные - события на шаге оформления
- Путь цены: от типа цены до суммы в корзине - откуда берётся сумма позиций
- Статусы заказов: смена из кода, события и сопоставление с 1С - что означает статус заказа
- Каталог и продажи - устройство продаж целиком