У нас сеть из семи магазинов. Заказы на самовывоз прилетают, но смотреть 1С каждые пять минут - так себе удовольствие. Менеджер в головном офисе обработал заказ, выставил статус “собрать” - а магазин узнает об этом только если сам зайдёт и проверит, либо менеджер должен ему сообщить.

При этом в компании уже давно работает корпоративный VK WorkSpace / VK Teams. Все магазины сидят в рабочих чатах, никаких сторонних мессенджеров.

И одни админ магазина спрашивает:

“Можно, чтобы когда заказ ставят на сборку, нам приходило уведомление. Чтобы мы не караулили 1С?”

Смысл простой:

  • Клиент оформил заказ
  • Менеджер проверяет
  • Ставит статус P (собрать)
  • 1C и Битрикс синхронизируется
  • Битрикс понимает “ага, это самовывоз, магазин №X”
  • Смотрит UF_CHATID этого магазина
  • Отправляет сообщение в VK Teams через бота
  • Сотрудник сразу идет собирать. И всё. Никаких пропущенных заказов, никаких “ой, мы не заметили”, никаких обновлений 1С каждые 2 минуты.

1. Событие, на котором всё держится

Вешаем обработчик на: `

OnSaleOrderSaved

Это одно из самых стабильных событий в Bitrix - оно срабатывает каждый раз, когда заказ сохраняется, независимо от того, меняли ли статус через:

  • 1С;
  • Админку Bitrix;
  • API;
  • Интеграцию. Это критично, потому что в процессе участвуют 1С Битрикс, и мы должны ловить, изменения, которые приходят “снаружи”, а не только через админку.

2. Определяем, что статус реально изменился

Что бы не слать уведомления при каждом сохранении заказа, мы сравниваем:

  • старый статус (из getOriginalValues())
  • новый статус ( getField("STATUS_ID")) Обрабатываем только если статус реально изменился и только если он стал:
  • P - собрать заказ
  • B - заказ отменён Это защищает от дублей и мусорных триггеров.

3. Привязка к магазину через доставку “Самовывоз”

Из коллекции отгрузок (ShipmentColletction) извлекаем:

  • доставку заказа
  • store ID
  • а далее - магазин из каталога (CCatalogStore) В магазин (склад) добавляем пользовательское поле:
UF_CHATID

В него будем записывать электронную почту магазина. Это номер чата в VK Teams, куда нужно отправлять уведомления. Такая связь “Магазин - Чат” универсальна, масштабируема и не требует хардкода.


4. Формируем данные для уведомления

Из заказа собираем:

  • номер заказа
  • ФИО клиента
  • сумму
  • комментарий менеджера
  • состав заказа (название + количество)
  • название магазина Это превращаем в форматированный HTML-текст для VK Teams Пример для статуса “собрать”:
⚠️ Соберите заказ №12345
────────────────────
 Клиент: Иван Иванов
 Сумма: 3 250 ₽
 Магазин: Магазин №X
────────────────────
🧾 Состав заказа:
[Фен - 1 шт]
[Шампунь - 2 шт]

5. Отправка уведомления в VK Teams

VK WorkSpace (MyTeam) предоставляет простой REST API:

/bot/v1/messages/sendText

Мы передаём параметры:

  • токен бота
  • chatid
  • текст сообщения
  • parseMode = HTML Происходит обычный GET-запрос через cURL. Сообщение появляется мгновенно - сотрудники магазина видят как не прочитанное в трее.

6. Пример интеграционного обработчика для статусов заказов

Вот минимальный рабочий пример класса, который:

  • ловит изменения статуса,
  • проверяет, что это самовывоз,
  • находит магазин,
  • читает UF_CHATID
  • формирует сообщение,
  • отправляет его в VK Teams.
<?php
namespace Company\EventHandler;  
 
use Bitrix\Main\Loader;
use Bitrix\Sale;  
 
class SendOrderToStore
{
 
    public static function SendOrderNotifyToStore(\Bitrix\Main\Event $event)
    {
 
        $order = $event->getParameter("ENTITY");
        $isNew = $event->getParameter("IS_NEW");
        
        if ($isNew) return; 
        
        $statusNew = $order->getField("STATUS_ID");
        $fieldValues = $order->getFields()->getOriginalValues();
        $statusOld = $fieldValues['STATUS_ID'] ?? null;        
        
        $logFile = $_SERVER['DOCUMENT_ROOT'] . '/vk_event.log';
 
        file_put_contents($logFile, date('[Y-m-d H:i:s]') . " → Order {$order->getId()} {$statusOld} → {$statusNew}\n", FILE_APPEND);
 
        if (!in_array($statusNew, ['P', 'B']) || $statusOld === $statusNew) return;        
        self::processStoreNotification($order->getId(), $statusNew);
    }
 
 
    protected static function processStoreNotification($orderId, $statusNew)
    {
        Loader::includeModule('sale');
        Loader::includeModule('catalog');
        $order = Sale\Order::load($orderId);    
                    
        if (!$order) return; 
        
        $propertyCollection = $order->getPropertyCollection();
        $buyerName = 'Клиент';
 
        foreach ($propertyCollection as $property) {
        
            $propCode = $property->getField('CODE');
            $propValue = trim($property->getValue());
            if (in_array($propCode, ['FIO', 'CONTACT_NAME', 'NAME', 'FULL_NAME']) && $propValue) {
                $buyerName = $propValue;
                break;
            }
        }  
 
        $price = number_format($order->getPrice(), 0, '', ' ');
        $managerComment = trim($order->getField('COMMENTS'));
        $shipmentCollection = $order->getShipmentCollection();  
 
        foreach ($shipmentCollection as $shipment) {
 
            if ($shipment->isSystem()) continue;
            $deliveryService = $shipment->getDelivery();
            if (!$deliveryService || stripos($deliveryService->getName(), 'Самовывоз') === false) continue; 
 
            $storeId = $shipment->getStoreId();
            if (!$storeId) continue; 
 
            $store = \CCatalogStore::GetList([], ['ID' => $storeId], false, false, ['ID','TITLE','UF_CHATID'])->Fetch();
 
            if (!$store || !$store['UF_CHATID']) continue;
            
            $chatId = trim($store['UF_CHATID']);
            $storeTitle = $store['TITLE'];
            $basket = $order->getBasket();
            $itemsList = [];
            
            foreach ($basket as $item) {
                $name = $item->getField('NAME');
                $quantity = (float)$item->getQuantity();
                $itemsList[] = "[{$name} - {$quantity} шт]";
            }
            
            $itemsText = implode("\n", $itemsList);
            $commentText = $managerComment ? "\nКомментарий: {$managerComment}" : '';            
           
            if ($statusNew === 'P') {
                $text = "⚠️ Соберите заказ №<b>{$orderId}</b>\n".
                    "────────────────────\n".
                    " Клиент: {$buyerName}\n".
                    " Сумма: <b>{$price} ₽</b>\n".
                    " Магазин: {$storeTitle}{$commentText}\n".
                    "────────────────────\n".
                    "🧾 Состав заказа:\n{$itemsText}";
            } elseif ($statusNew === 'B') {
                $text = "❌ Заказ: №<b>{$orderId}</b> отменён — расформируйте заказ\n".
                    "────────────────────\n".
                    "Клиент: {$buyerName}\n".
                    "Сумма: <b>{$price} ₽</b>\n".
                    "Магазин: {$storeTitle}{$commentText}\n".
                    "────────────────────\n".
                    "🧾 Состав заказа:\n{$itemsText}";
 
            }
            self::sendVkMessage($chatId, $text);
        }
    }
 
    protected static function sendVkMessage($chatId, $text)
    {
        $token = getenv('MYTEAM_BOT_TOKEN');
        if (!$token) return;
        $url = 'https://myteam.mail.ru/bot/v1/messages/sendText'; 
        $params = http_build_query([
            'token'  => $token,
            'chatId' => $chatId,
            'text'   => $text,
            'parseMode' => 'HTML'
        ]);        
        $curl = curl_init("{$url}?{$params}");
        curl_setopt_array($curl, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
        ]); 
        $response = curl_exec($curl);
        curl_close($curl);
    }
}

В итоге всё стало чуть проще…