Заказчик - интернет-магазин автозапчастей. Продажи идут через торговую платформу ABCP, а клиенты и возвраты ведутся в amoCRM. Возврат живёт в двух системах сразу: заявка приходит письмом и превращается в сделку в воронке «Возвраты», а фактический статус позиции заказа хранится в ABCP - его видят склад, выдача и поставщики.
Пока связи не было, менеджер по возвратам работал в двух окнах: принял решение в сделке, пошёл в ABCP и руками переставил статус позиции. В обратную сторону было хуже: когда склад или автоматика ABCP меняли статус, сделка в amoCRM об этом не узнавала, пока кто-нибудь не заглянет. Статусы расходились, сделки зависали на этапах, автоматизация воронки не срабатывала.
Работа шла двумя этапами с перерывом больше месяца.
Этап 1. Менеджер меняет поле «Статус для клиента» в сделке - у нужной позиции заказа в ABCP должен появиться соответствующий статус. Ходить в ABCP, чтобы переставить статус, больше не нужно. Соответствие значений поля и статусов ABCP должно правиться без программиста.
Этап 2. Обратное направление: любое изменение статуса позиции в ABCP, сделанное сотрудником или автоматикой площадки, должно само появляться в сделке - заполнять поле «Статус из ABCP» и переводить сделку на нужный этап воронки, чтобы дальше отработала штатная автоматизация amoCRM клиента. Ограничения: тариф amoCRM без триггера «изменение поля», Salesbot под это нет, у ABCP нет вебхуков - только опрос по API.
Жёсткие требования: это боевой продакшн с несколькими тысячами сделок. Нельзя портить реальные сделки, дублировать события, откатывать статусы, терять события при перезапусках и выкатках. Изменения должны доезжать за минуты. Разворачивать на сервере заказчика, деплой без ручных шагов, включение по частям с возможностью откатить.
Сервис на Node.js и TypeScript (Express), один процесс под pm2 на VPS заказчика. Оба направления живут в нём: это осознанное решение, а не экономия.
Прямой канал, amoCRM => ABCP. amoCRM шлёт вебхук на каждое изменение сделки, не уточняя, что именно поменялось. Сервис отвечает 200 сразу и обрабатывает асинхронно: дочитывает сделку через REST API amoCRM v4, фильтрует по воронке и площадке, берёт значение поля, через конфиг превращает его в ID статуса ABCP, находит заказ по ID позиции (ABCP ищет только по окнам дат: сначала по дате обновления, затем по дате создания) и ставит статус через ABCP API 1.0. Отметка отправленного хранится на диске: повторы и устаревшие значения не уходят.
Обратный канал, ABCP => amoCRM. Каждые 5 минут сервис берёт открытые сделки воронки с числовым ID позиции. Реестр не хранится, а выводится из воронки, поэтому не может с ней разойтись. По позициям пачками запрашивается история статусов. По каждой позиции хранится отпечаток последней обработанной записи: код статуса, дата-время, автор. Всё, что выше отпечатка, - новое. Из нового берутся только статусы белого списка и только чужие записи: то, что поставила сама интеграция, событием не считается, иначе получился бы цикл amo => ABCP => amo. Поле и этап уходят одним PATCH, доставка атомарна. При первой встрече с позицией только записывается отпечаток - старая история в сделку не льётся.
Защита от эха. Наш же PATCH возвращается вебхуком, и прямой канал отправил бы в ABCP устаревшее значение, откатывая статус, который менеджер только что поставил. EchoGuard - метка в памяти «мы только что записали»: ставится до PATCH, гасит вебхуки с тем же значением, настоящее изменение поля метку снимает. Работает потому, что оба канала в одном процессе - без файлов между процессами и без гонок.
Устойчивость к API. amoCRM: очередь с ограничением 5 запросов в секунду при лимите 7, повтор при 429 и сетевых ошибках. ABCP: ошибки приходят как HTTP 200 с кодом внутри; чужой или нечисловой ID позиции роняет весь пакетный запрос, поэтому такие ID отсекаются заранее, отравленная пачка делится пополам, а позиции чужого аккаунта уходят на шестичасовую передышку. Позиции, исчезнувшие из воронки, забываются через сутки, а не сразу, чтобы не потерять событие на мерцании.
Настройка без программиста. Три JSON-конфига: значение поля => статус ABCP, белый список статусов, статус => этап воронки. Перечитываются на каждом обращении, правки без перезапуска. Диагностика одной командой: поля и значения списков, отпечатки, история позиции с отметкой «попадёт в сделку», проверка готовности к включению.
Ограничение тарифа обошёл так: сделку по этапам переводит сам сервис, а автоматизацию клиент вешает на «после перехода в этап» - это событие на его тарифе есть. Проверено на живой сделке: робот клиента срабатывает от перевода через API.
Выкатка. Push в main => GitHub Actions => ssh на сервер: git pull, npm ci, сборка, pm2 reload и проверка /health изнутри сервера - деплой падает, если сервис не поднялся. Отдельный read-only workflow диагностики: статус процесса, здоровье, хвост логов. Включение обратного канала поэтапное: сначала DRY-RUN, когда сервис только пишет в лог, что сделал бы, и не сжигает события, потом боевой режим одной переменной окружения.
Обе системы синхронны. Менеджер ставит статус в сделке - статус в ABCP меняется сам. Склад или автоматика ABCP меняет статус - через несколько минут сделка получает поле «Статус из ABCP» и переезжает на нужный этап, дальше отрабатывает автоматизация клиента. Менеджер по возвратам работает в одном окне amoCRM. В первые же дни живые сделки прошли путь «На согласовании => Принят поставщиком => Согласован» без единого ручного действия.
Ничего не портится. За время эксплуатации - ни одного дублирующего события, отката статуса или потерянного события. Записи интеграции не плодят лишних записей в истории ABCP, чужие сделки (другие площадки, второй аккаунт ABCP) отфильтрованы и не трогаются.
Масштаб: около 5 000 сделок в воронке, около 800 активных позиций под наблюдением, опрос каждые 5 минут, 10 статусов возврата в обе стороны, 4 из них двигают сделку по этапам. Процесс работает без перезапусков неделями, на момент съёмки кадров - 33 дня аптайма при 130 МБ памяти. Весь сервис - около 2 300 строк TypeScript и две зависимости: Express и dotenv.
Бонус при внедрении: ревизия данных нашла около 200 сделок с задублированными или мёртвыми ID позиций (часть заводил AI-парсер почты заказчика) - список выгружен для чистки.
Проект продолжается: обсуждается третий этап - управление этапами только через поле, примечания с данными позиции из ABCP прямо в сделке, статусы поставщиков.
Публичной ссылки нет: сервис без интерфейса работает внутри инфраструктуры заказчика, а amoCRM закрыта. В портфолио - обезличенные кадры amoCRM, настоящие логи, код и схема.