Перейти к содержимому

Заказ изнутри - коллекции, пересчёт, сохранение, события

Разбираем устройство заказа: из каких коллекций он собран, когда пересчитываются суммы, что возвращает сохранение и в каком порядке срабатывают события продаж.

Механика

Заказ - это дерево связанных объектов, а не одна строка таблицы. У него есть корзина с позициями, коллекция оплат, коллекция отгрузок и коллекция свойств, и каждая живёт своей жизнью.

Изменения копятся в объектах заказа и уходят в базу одним сохранением. Пока сохранение не вызвано, в базе лежит прежнее состояние заказа, и любая внешняя выборка видит именно его.

Суммы заказа пересчитываются не после каждой правки отдельного поля. Пересчёт запускается явно либо в момент сохранения, и это объясняет знаменитое «сумма в объекте одна, а в базе другая».

У позиции корзины может стоять признак своей ручной цены. Такая строка выпадает из пересчёта целиком: ни скидки каталога, ни правила корзины к ней больше не применяются.

Оплата и отгрузка - это отдельные документы внутри самого объекта заказа. Признак оплаты заказа складывается из оплат, а признак отгрузки - из отгрузок, и напрямую эти поля не правят.

Статус заказа сам по себе никак не связан с оплатой и отгрузкой. Он описывает движение заказа по процессу магазина, и связь со свойствами оплаты настраивают своим кодом или бизнес-правилами.

Сохранение заказа возвращает объект результата, а не простой признак успеха. В нём лежат ошибки всех уровней дерева, и без их чтения заказ молча остаётся несохранённым.

События модуля продаж срабатывают вокруг сохранения заказа в понятном фиксированном порядке. Сначала события до записи, где ещё можно вмешаться, затем сама запись, затем события после неё.

Отдельная сущность внутри заказа - это его свойства покупателя и доставки. Они живут своей коллекцией, привязаны к типу плательщика и читаются по коду свойства, а не по его названию.

Понимание этого дерева экономит больше всего времени при разборе жалоб. Вопрос «почему сумма не та» превращается в понятную последовательность проверок от позиции корзины до итога заказа.

Шаги

  1. Загрузить заказ и понять, какие именно коллекции понадобятся для нужной задачи.
  2. Менять данные в объектах коллекций, а не запросами в таблицы продаж.
  3. Явно запускать пересчёт, если суммы должны обновиться до сохранения заказа.
  4. Читать объект результата сохранения и показывать ошибки, а не глотать их.
  5. Вешать свою логику на события до записи, если нужно вмешаться в заказ.

Код

Загружаем заказ и обходим коллекции:

\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()); // числа должны совпасть

Полезная привычка при разборе - печатать состояние заказа до и после сохранения. Две строки в журнале показывают, что именно изменилось, и снимают половину вопросов к платформе.

Ограничения

Прямые запросы в таблицы модуля продаж ломают согласованность его данных. Сумма заказа, признаки оплаты и резервы считаются кодом модуля, и правка строк мимо него оставляет заказ в противоречивом состоянии.

Позиция корзины со своей ручной ценой не участвует в пересчёте вовсе. Акции и купоны на неё не действуют до тех пор, пока признак ручной цены не снят явно.

Заказ, который уже уехал в учётную систему, менять на сайте рискованно. Правки на двух сторонах спорят между собой, и правило «чья версия главнее» согласуют заранее.

Тяжёлая работа в событиях сохранения замедляет оформление. Обращения к внешним сервисам и рассылку писем выносят в фоновое выполнение, а не делают в обработчике.

Отмена заказа не возвращает резервы и оплаты сама по себе. Эти шаги выполняют явно, иначе товар остаётся зарезервированным, а деньги - не возвращёнными.

Загрузка заказа целиком стоит нескольких запросов к базе данных. В списках и в отчётах поэтому берут табличную выборку, а объект заказа собирают только там, где его действительно меняют.

Типичные проблемы

Сумма заказа в коде не совпадает с суммой в базе.

Пересчёт не выполнялся, а изменения ещё не сохранены в базу данных. Суммы читают после сохранения либо после явного запуска пересчёта заказа.

Заказ не сохранился, а код не показал ни одной ошибки.

Результат сохранения не проверялся, а ошибки лежат именно в нём. Объект результата читают всегда и показывают его сообщения человеку или в журнал.

Скидка не применилась к позиции заказа.

У позиции стоит признак своей цены, и она выпала из пересчёта скидок. Признак ручной цены снимают, если позиция должна участвовать в акциях.

Признак оплаты не выставляется при добавлении оплаты.

Оплата создана, но не отмечена оплаченной, и заказ считает её ожидаемой. Признак оплаты заказа складывается из отмеченных оплат, а не из их наличия.

Оформление заказа стало заметно медленным.

В обработчике события сохранения выполняется медленный запрос к какому-то внешнему сервису. Такие медленные вызовы выносят в фоновое выполнение уже после ответа покупателю.

После правки заказа запросом сломались отчёты.

Строки таблиц продаж менялись напрямую, мимо кода модуля и его пересчётов. Заказ правят только через его объекты, даже если запрос кажется проще.

Частые вопросы

Почему сумма не меняется сразу после правки?

Пересчёт выполняется явно или при сохранении, а не после каждой правки поля. Это осознанное решение: пересчёт на каждое изменение был бы слишком дорогим.

Что возвращает сохранение заказа?

Объект результата с признаком успеха и списком ошибок всех уровней дерева. Без его проверки ошибки остаются незамеченными, а заказ - несохранённым.

Можно ли править заказ запросами к базе?

Технически можно, но так ломаются пересчёты, резервы и отчёты. Заказ меняют через его объекты, даже если это выглядит длиннее.

Чем статус отличается от признака оплаты?

Статус описывает движение заказа по процессу магазина и меняется людьми или кодом. Признак оплаты складывается из документов оплаты и меняется вместе с ними.

Где вешать свою проверку заказа?

На событие до записи: там ещё можно отменить сохранение с понятной ошибкой. События после записи годятся для уведомлений и для фоновой работы.

Смежное

Первоисточник