Уведомления по заказам
Модуль sale отправляет штатные письма при создании и изменении заказа. Если штатного письма недостаточно, разработчик может подписаться на событие заказа и вызвать собственное почтовое событие через Bitrix\Main\Mail\Event.
Используйте штатное уведомление, когда нужно изменить получателя, тему или текст существующего письма. Собственный обработчик нужен для отдельного письма, другого канала связи или дополнительного действия во внешней системе.
Как формируется уведомление
Уведомление по заказу формируется в три этапа:
-
Изменение заказа создает повод для уведомления. Например, заказ сохранен впервые, полностью оплачен, отменен или переведен в другой статус.
-
Модуль
saleили пользовательский обработчик формирует почтовое событие и передает поля заказа. -
Почтовая система ставит событие в очередь. Во время обработки очереди она выбирает активные шаблоны по коду события и сайту, подставляет поля в макросы и отправляет сформированные письма.
Событие модуля sale и почтовое событие решают разные задачи. Событие модуля возникает при изменении PHP-объекта. Почтовое событие связывает подготовленные поля с почтовым шаблоном.
Выбрать штатное или собственное уведомление
|
Задача |
Решение |
|
Изменить тему, текст, отправителя или получателя существующего письма |
Изменить почтовый шаблон штатного события |
|
Включить или отключить письмо о переходе в конкретный статус |
Настроить признак уведомления для статуса и активность почтового шаблона |
|
Добавить в штатное письмо данные проекта |
Подготовить дополнительные поля до отправки, если для выбранного штатного события предусмотрена точка расширения |
|
Отправить отдельное письмо по бизнес-условию |
Подписаться на событие заказа и вызвать собственное почтовое событие |
|
Передать изменение во внешнюю систему или другой канал |
Выполнить отдельное действие в обработчике или поставить задачу в очередь |
Не отправляйте копию штатного письма из события заказа. Иначе покупатель может получить два сообщения об одном изменении.
Настроить штатное письмо
Штатное уведомление связано с типом почтового события и набором полей заказа. Для изменения такого письма не регистрируйте обработчик заказа и не создавайте второй тип события.
Класс Bitrix\Sale\Notify формирует поля штатных событий и передает их почтовой системе. При обычном сохранении заказа эти методы вызывает сам модуль. Не вызывайте их повторно из пользовательского обработчика: это может создать дубликат уведомления.
Ниже перечислены основные штатные события для рассматриваемых сценариев.
|
Код почтового события |
Когда формируется событие |
|
|
При создании заказа |
|
|
При полной оплате заказа |
|
|
При отмене заказа |
|
|
При разрешении доставки заказа |
|
|
При изменении трек-номера отгрузки |
|
|
При переходе заказа в статус |
Порядок настройки:
-
В списке типов почтовых событий найдите событие модуля
sale, которое соответствует изменению заказа. -
Откройте привязанный к нему почтовый шаблон для нужного сайта.
-
Проверьте активность шаблона, сайт, язык, отправителя и получателя.
-
Измените тему или текст с помощью макросов, которые перечислены в описании типа события.
-
Выполните действие с тестовым заказом и проверьте появление события в журнале почтовой системы.
-
Убедитесь, что очередь обработала событие и для него найден шаблон.
Для письма о смене статуса дополнительно проверьте поле NOTIFY выбранного статуса. Значение N исключает статус из штатного сценария уведомления, даже если почтовый шаблон активен.
Если для одного типа события активно несколько подходящих шаблонов, почтовая система может сформировать несколько писем. Проверяйте все активные шаблоны, которые соответствуют событию и сайту заказа.
Настройка типов событий, шаблонов, макросов и журнала описана в статье Работа с почтой.
Изменить поля перед штатной отправкой
Модуль sale вызывает события классического API перед созданием штатного почтового события. Такие обработчики получают позиционные аргументы, а код события и массив полей — по ссылке. Обработчик может изменить эти значения. Если он возвращает false, штатное письмо не ставится в очередь.
События из таблицы относятся к классическому API. Используйте их только для изменения или отмены уже подготовленного штатного письма. Собственную бизнес-логику подключайте к объектным событиям D7 из раздела Выбрать событие заказа.
|
Штатное письмо |
Событие классического API |
|
Новый заказ |
|
|
Полная оплата |
|
|
Отмена заказа |
|
|
Смена статуса |
|
|
Разрешение доставки |
|
Для регистрации события классического API используйте метод совместимости addEventHandlerCompatible(). Параметры обработчика содержат:
-
$orderId— внутренний идентификатор заказа, -
$eventName— код почтового события, -
$fields— значения макросов.
Обработчик из примера добавляет поле PROJECT_CODE в штатное письмо об оплате:
\Bitrix\Main\EventManager::getInstance()->addEventHandlerCompatible(
'sale',
'OnOrderPaySendEmail',
static function (
int $orderId,
string &$eventName,
array &$fields
): bool
{
$fields['PROJECT_CODE'] = 'online-store';
return true;
}
);
Добавьте макрос #PROJECT_CODE# в описание типа события и используйте его в почтовом шаблоне.
Настроить собственное уведомление
Для собственного уведомления создайте тип почтового события, выберите событие заказа и зарегистрируйте обработчик. Затем подготовьте поля заказа и вызовите почтовое событие.
Подготовить почтовое событие и шаблон
Для собственного письма заранее создайте тип почтового события и хотя бы один активный шаблон. Код типа события должен совпадать со значением EVENT_NAME, которое обработчик передает в Bitrix\Main\Mail\Event::send().
Для каждого почтового события передайте отдельный набор полей. Для типа события SALE_CUSTOM_ORDER_PAID:
-
ORDER_ID— внутренний идентификатор заказа, -
ORDER_ACCOUNT_NUMBER— номер заказа для покупателя, -
ORDER_PRICE— стоимость заказа, -
ORDER_CURRENCY— код валюты заказа, -
EMAIL_TO— адрес получателя.
Для типа события SALE_CUSTOM_ORDER_FINISHED:
-
ORDER_ID— внутренний идентификатор заказа, -
ORDER_ACCOUNT_NUMBER— номер заказа для покупателя, -
STATUS_ID— новый код статуса, -
OLD_STATUS_ID— предыдущий код статуса, -
EMAIL_TO— адрес получателя.
Для каждого кода создайте отдельный тип события и активный шаблон. Параметр LID не входит в C_FIELDS: обработчик передает в параметре LID идентификатор сайта заказа, по которому почтовая система выбирает шаблон.
Привяжите шаблон к нужному сайту. При отправке передавайте идентификатор этого сайта в параметре LID. Настройка типов событий, шаблонов и макросов описана в статье Работа с почтой.
Выбрать событие заказа
Подписывайтесь на событие, которое соответствует бизнес-смыслу уведомления.
|
Сценарий |
Событие |
|
При первом сохранении заказа |
|
|
При установке или снятии признака полной оплаты заказа |
|
|
При отмене или восстановлении заказа |
|
|
При изменении статуса заказа |
|
|
При изменении статуса отгрузки |
|
|
При сохранении конкретной оплаты |
|
|
При сохранении конкретной отгрузки |
|
Состав параметров и момент вызова этих событий описаны в разделе События заказа и связанных объектов.
Для оплаты проверяйте текущее состояние заказа через Order::isPaid(): событие сообщает и об установке, и о снятии признака оплаты. Для отмены используйте Order::isCanceled(). В событиях смены статуса сравнивайте параметры VALUE и OLD_VALUE.
Зарегистрировать обработчик
Чтобы регистрировать обработчик при каждом запросе, добавьте код в /local/php_interface/init.php или в подключаемый из него файл. Перед регистрацией убедитесь, что модуль sale доступен.
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
if (Loader::includeModule('sale'))
{
EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderPaid',
'sendPaidOrderNotification'
);
}
Обработчик получит объект события Bitrix\Main\Event. Реализация функции sendPaidOrderNotification() приведена в разделе Отправить письмо после оплаты.
Отправить письмо после оплаты
Обработчик получает заказ в параметре ENTITY. Используйте переданный объект. Он уже содержит состояние, для которого вызвано событие.
Обработчик из примера отправляет собственное письмо только после установки признака полной оплаты. До запуска кода создайте тип события SALE_CUSTOM_ORDER_PAID и его почтовый шаблон.
use Bitrix\Main\Event;
use Bitrix\Main\Mail\Event as MailEvent;
use Bitrix\Sale\Order;
function sendPaidOrderNotification(Event $event): void
{
/** @var Order $order */
$order = $event->getParameter('ENTITY');
if (!$order->isPaid())
{
return;
}
$emailProperty =
$order
->getPropertyCollection()
->getUserEmail()
;
if (!$emailProperty)
{
return;
}
$email = (string) $emailProperty->getValue();
if ($email === '')
{
return;
}
$sendResult = MailEvent::send([
'EVENT_NAME' => 'SALE_CUSTOM_ORDER_PAID',
'LID' => $order->getSiteId(),
'C_FIELDS' => [
'ORDER_ID' => $order->getId(),
'ORDER_ACCOUNT_NUMBER' => $order->getField('ACCOUNT_NUMBER'),
'ORDER_PRICE' => $order->getPrice(),
'ORDER_CURRENCY' => $order->getCurrency(),
'EMAIL_TO' => $email,
],
]);
if (!$sendResult->isSuccess())
{
error_log(implode('; ', $sendResult->getErrorMessages()));
}
}
Значение свойства email зависит от типа плательщика и настроек свойств заказа. Если email не найден, обработчик завершает работу без отправки. Не подставляйте адрес текущего авторизованного пользователя. Событие может выполняться в административном, фоновом или интеграционном сценарии.
Параметр LID определяет сайт, для которого почтовая система ищет шаблон. Используйте сайт заказа, а не глобальную константу текущего запроса.
Отправить письмо при смене статуса
Событие OnSaleStatusOrderChange передает новый и предыдущий коды статуса. Отправляйте письмо только для нужного перехода.
До регистрации обработчика подключите модуль sale и создайте тип события SALE_CUSTOM_ORDER_FINISHED с полями из раздела Подготовить почтовое событие и шаблон.
Зарегистрируйте отдельный обработчик для функции sendOrderStatusNotification():
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleStatusOrderChange',
'sendOrderStatusNotification'
);
Реализуйте функцию sendOrderStatusNotification(), которая проверяет переход в конечный статус и отправляет почтовое событие:
use Bitrix\Main\Event;
use Bitrix\Main\Mail\Event as MailEvent;
use Bitrix\Sale\Order;
function sendOrderStatusNotification(Event $event): void
{
$newStatusId = (string) $event->getParameter('VALUE');
$oldStatusId = (string) $event->getParameter('OLD_VALUE');
if ($newStatusId !== 'F' || $oldStatusId === $newStatusId)
{
return;
}
/** @var Order $order */
$order = $event->getParameter('ENTITY');
$emailProperty =
$order
->getPropertyCollection()
->getUserEmail()
;
if (!$emailProperty)
{
return;
}
$email = (string) $emailProperty->getValue();
if ($email === '')
{
return;
}
$sendResult = MailEvent::send([
'EVENT_NAME' => 'SALE_CUSTOM_ORDER_FINISHED',
'LID' => $order->getSiteId(),
'C_FIELDS' => [
'ORDER_ID' => $order->getId(),
'ORDER_ACCOUNT_NUMBER' => $order->getField('ACCOUNT_NUMBER'),
'STATUS_ID' => $newStatusId,
'OLD_STATUS_ID' => $oldStatusId,
'EMAIL_TO' => $email,
],
]);
if (!$sendResult->isSuccess())
{
error_log(implode('; ', $sendResult->getErrorMessages()));
}
}
Код F обозначает финальный статус заказа в стандартной конфигурации. Если проект использует другой бизнес-переход, замените условие на код из настроек проекта.
Если штатное уведомление для этого статуса включено, собственное событие должно отправлять другое сообщение или использовать другой канал. Признак NOTIFY в справочнике статусов управляет участием статуса в штатном уведомлении о смене статуса. Подробнее о поле NOTIFY читайте в статье Статусы и события.
Реализовать обработчик
Обработчик должен использовать состояние заказа из события и учитывать повторные вызовы. Долгие операции переносите в очередь, а бизнес-условия отделяйте от текста письма.
Получить данные заказа
Берите данные из объекта, который передан в событие. Не загружайте заказ повторно внутри этого обработчика: другая операция уже могла изменить сохраненное состояние.
Для уведомления нужны основные данные заказа и его текущее состояние. Получите их из переданного объекта:
|
Данные |
Как получить |
|
Внутренний идентификатор заказа |
|
|
Номер заказа для покупателя |
|
|
Идентификатор сайта |
|
|
Стоимость и валюта |
|
|
Признак полной оплаты |
|
|
Признак отмены |
|
|
Значения свойств заказа |
|
|
Оплаты и отгрузки |
|
Не передавайте в почтовый шаблон весь объект заказа. Сформируйте плоский массив C_FIELDS из строковых и числовых значений. Почтовая система подставляет эти значения в макросы шаблона. Такой контракт явно фиксирует данные, которые обработчик передает шаблону. При изменении набора макросов согласуйте состав C_FIELDS с шаблоном.
Проверяйте наличие необязательных данных. Свойство email, имя покупателя, телефон и адрес зависят от типа плательщика и настроек конкретного сайта.
Отменить действие до сохранения
События после сохранения подходят для уведомлений, но не для проверки возможности сохранить заказ. Если бизнес-условие должно остановить сохранение, выполните проверку в OnSaleOrderBeforeSaved и верните Bitrix\Main\EventResult с ошибкой.
Обработчик из примера останавливает сохранение заказа с нулевой стоимостью:
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Bitrix\Main\EventResult;
use Bitrix\Sale\Order;
use Bitrix\Sale\ResultError;
EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderBeforeSaved',
static function (Event $event): EventResult
{
/** @var Order $order */
$order = $event->getParameter('ENTITY');
if ($order->getPrice() <= 0)
{
return new EventResult(
EventResult::ERROR,
new ResultError('Стоимость заказа должна быть больше нуля')
);
}
return new EventResult(EventResult::SUCCESS);
}
);
Не отправляйте письмо до успешного завершения сохранения. Иначе покупатель может получить уведомление об изменении, которое не попало в базу данных.
После успешного сохранения используйте профильное событие: OnSaleOrderSaved, OnSaleOrderPaid, OnSaleOrderCanceled или событие смены статуса.
Исключить повторную отправку
Повторный запрос, фоновая задача или интеграция могут снова запустить обработку одного состояния заказа. Условие в обработчике должно проверять не только новое состояние, но и факт уже выполненного действия.
Выберите способ защиты в зависимости от архитектуры проекта:
-
сравнивайте новое и прежнее значения из параметров события;
-
храните уникальный ключ уведомления, например сочетание заказа, типа сообщения и целевого состояния;
-
создавайте в очереди задачу с уникальным ключом и не добавляйте дубликат;
-
помечайте успешную отправку в отдельном хранилище проекта.
До постановки сообщения в очередь запишите уникальный ключ в хранилище с уникальным индексом. Например, индекс может объединять идентификатор заказа, тип уведомления и целевое состояние. Обычная проверка «записи еще нет» без уникального ограничения не защищает от двух параллельных обработчиков.
Разделяйте состояния обрабатывается и отправлено. Если постановка почтового события завершилась ошибкой, снимите резерв или переведите запись в состояние, которое допускает контролируемую повторную попытку. Не создавайте новый ключ для каждой попытки, иначе защита от дублей потеряет смысл.
Не используйте поле комментария или другое пользовательское поле заказа как технический флаг без отдельного соглашения проекта. Повторное сохранение заказа из обработчика может снова вызвать тот же обработчик.
Предотвратить рекурсию и долгую обработку
Не вызывайте $order->save() из OnSaleOrderSaved и других обработчиков после сохранения без отдельной защиты. Повторное сохранение запускает новый цикл событий и может создать рекурсию или дубликаты уведомлений.
Если нужно изменить объект в текущем цикле сохранения, используйте OnSaleOrderBeforeSaved, проверьте текущее значение и не вызывайте save() внутри обработчика.
Отправку во внешний сервис и другую долгую операцию переносите в очередь. Обработчик события должен передать идентификатор заказа, тип уведомления, целевое состояние и уникальный ключ операции. Затем обработчик ставит задачу и завершает работу. Фоновый процесс повторно загружает заказ по идентификатору и проверяет, что состояние еще соответствует условию.
Проверить результат
Метод Bitrix\Main\Mail\Event::send() создает почтовое событие. Проверьте объект результата сразу после вызова и запишите ошибки в журнал приложения.
Успешный результат метода означает, что почтовая система приняла событие. Он не подтверждает доставку письма получателю. Для диагностики проверьте:
-
Совпадает ли
EVENT_NAMEс кодом типа события. -
Есть ли активный почтовый шаблон для сайта из
LID. -
Переданы ли все макросы, которые использует шаблон.
-
Появилось ли событие в журнале почтовой системы и обработала ли его очередь.
-
Нет ли ошибки транспорта или неверного адреса получателя.
Не используйте sendImmediate() в обычном обработчике заказа только ради мгновенной отправки. Синхронная отправка увеличивает время сохранения заказа и связывает бизнес-операцию с доступностью почтового транспорта.
Разделить бизнес-логику и текст письма
Обработчик события должен проверить условие отправки, выбрать получателя и подготовить поля. Тему, оформление и текст храните в почтовом шаблоне.
Такое разделение позволяет:
-
менять текст без изменения PHP-кода,
-
создавать разные шаблоны для сайтов и языков,
-
повторно использовать один тип события для нескольких активных шаблонов,
-
проверять бизнес-условие отдельно от верстки письма.
Если уведомление относится к внешнему сервису, вынесите интеграцию в отдельный класс проекта. Обработчик события должен передать ему идентификатор заказа, тип изменения, прежнее и новое значения.