Неделя 135. Рассылки, которые дублились, и пять систем за шесть дней

Разбор этой статьи
Эту тему разобрали в подкасте. Слушай параллельно с чтением.
Есть недели, которые проживаешь на автопилоте: код, код, деплой. А бывают такие, после которых садишься и думаешь: «Мы точно это всё за шесть дней сделали?» Неделя 135 была из вторых. Пять крупных систем, больше 260 коммитов в тринадцати проектах. И посреди всего этого детективная история с рассылками, которая научила меня больше, чем любой учебник.
Я Дмитрий Дьяконов, основатель Botseller AI. Это пятый выпуск серии «Ретроспектива», бортжурнал в прошлое, от настоящего к первому коммиту. И первый, где я осознанно пишу не хронику коммитов, а то, что чувствовал, о чём сомневался и какие уроки вынес.

Детектив с рассылками: как одна буква ломала всё
Понедельник начался с тикета: «Клиенту пришло два одинаковых сообщения». Массовая рассылка в мессенджере, функция, которой пользуются каждый день. Когда она дублирует сообщения, это не баг. Это удар по репутации клиента и по нашей.
Первая мысль: «Наверное, случайно запустили дважды». Посмотрел логи. Нет, одна рассылка, один запуск. Но лиду с username @IvanPetrov сообщение ушло два раза. Начал копать список рассылки и увидел: в базе этот человек записан дважды, как @IvanPetrov и как @ivanpetrov. Разный регистр, разные записи, одно и то же лицо.
Дедупликация работала по точному совпадению строк. JavaScript и Python различают Ivan и ivan, для них это два разных человека. Для получателя рассылки разницы нет.

Первый коммит утра: 101 строка, полноценная дедупликация лидов при повторной рассылке. Но когда я дописал и протестировал, понял, что этого мало. Нужна case-insensitive дедупликация. Не if username in seen, а if username.lower() in seen. Двадцать строк и ещё один коммит через шесть часов.
Казалось бы, всё. Но к вечеру прилетело ещё: «Рассылка упала, бот-лист пустой». Оказалось, что UI позволяет запустить рассылку с нулём ботов в списке. Добавил валидацию. Две строки, ещё один коммит.
Потом всплыла проблема с sent_by_bot_id: при рассылке через несколько ботов мы теряли информацию о том, какой конкретно бот отправил сообщение. Ещё шесть строк. Ещё коммит.
Я помню ощущение этого дня. Чинишь одно, открывается следующее. Как снимаешь слой луковицы: под каждым багом лежит ещё один, смежный. Самое неприятное то, что ни один из этих багов не был сложным. Каждый занимал пять-двадцать строк. Но каждый прятался за предположением: «username уникален» (нет, регистр), «бот-лист не пустой» (а если UI не проверил?), «один бот = одно сообщение» (а если рассылка через три бота?).

Уроки, которые я не мог выучить из книг
Параллельно со мной Павел переписывал логику рассылок с другой стороны, из CRM. Он добавлял логирование зарезервированных сообщений, переписывал вебхук для верного адреса при рассылке, строил новый endpoint для рассылки прямо из CRM-интерфейса. К четвергу он выкатил полную переработку параллельных рассылок: 657 строк нового кода в шестнадцати файлах.
Мы работали над одной и той же системой, с двух сторон, не ломая друг другу руки. И вот тут первый урок недели, который я хочу зафиксировать:
Рассылки это не «отправить N сообщений». Это распределённая очередь с дедупликацией, маршрутизацией между ботами, обработкой ошибок и мониторингом статуса. Мы начинали с простого «пройдись по списку и отправь». К концу недели у нас была полноценная система с параллельными потоками, Redis-дедупом, статусами «в очереди / отправлено / ошибка» и fallback-логикой при сбое бота.
Подробнее о том, как устроены рассылки в мессенджерах, мы писали отдельно. Важно другое: этот переход от «скрипта» к «системе» не произошёл по плану. Он произошёл потому, что продакшен показал все углы, которых мы не видели на этапе «ну, просто отправим».
Партнёрская программа: когда продукт перерастает основателя
Совсем другая история. Параллельно. Всё в ту же неделю.

Меня давно мучила мысль: я не смогу лично продать Botseller каждому салону красоты, каждой стоматологии, каждому агентству недвижимости в стране. Не хватит ни рук, ни часов. Нужны люди, которые сделают это вместо меня, и которым это будет выгодно.
Партнёрская программа v1 была рефералкой: дай ссылку, получи процент. Просто, но ограниченно. Реферал не мотивирует строить бизнес вокруг твоего продукта. Нужна полноценная инфраструктура: уровни (ранги), автоматические комиссии, контракты, выплаты, отслеживание конверсий.
Неделя 135 это неделя, когда партнёрская программа стала системой. Восемь новых таблиц в базе данных: комиссии, контракты, выплаты, пиксельные события, история рангов, реферальные ссылки и клики, настройки. 5500 строк в бэкенде данных, 4200 строк в API, 3000 строк на фронтенде: кабинет партнёра, детальная статистика, конфигуратор условий.

Зачем я пишу об этом так подробно? Потому что строить партнёрку это не про код. Это про доверие. Ты даёшь внешним людям право представлять твой продукт. Ты автоматизируешь деньги: комиссии, выплаты, ранги. Каждая ошибка в расчёте комиссии это конфликт с партнёром. Каждый баг в отслеживании реферала это потерянные деньги для человека, который тебе доверился.
Я несколько раз переписывал логику расчёта рангов, прежде чем она показалась мне честной. Простой вопрос: «Что если партнёр привёл десять клиентов, но пять из них ушли через месяц? Его ранг должен упасть?» На него нет технического ответа. Есть только бизнес-решение, и его принимать мне.
Мы решили: ранг считается по активным клиентам. Привёл и удержал, растёшь. Привёл и потерял, стоишь. Это тяжелее для партнёров, но честнее для экосистемы. Подробнее об архитектуре партнёрской модели в статьях про White Label SaaS и заработок на AI-ботах.
Права сотрудников: «всем видно всё» больше не работает

Третья система недели. Она родилась не из плана, а из боли клиентов.
До этой недели в Botseller была одна роль: владелец. Он видит всё, управляет всем. Это нормально, когда у тебя один человек в компании. Но наши клиенты росли: салоны нанимали администраторов, агентства нанимали менеджеров, стоматологии нанимали рецепционистов. И все они работали через один аккаунт владельца.
«Мой администратор видит финансы». Такой тикет я получал раз в неделю. «Как закрыть менеджеру доступ к настройкам бота?» Ещё один. Каждый раз ответ был один: «Пока никак».
Неделя 135 закрыла это. RBAC (Role-Based Access Control) звучит сухо, но за этим стоит конкретная механика: ты создаёшь роль (например, «Менеджер по продажам»), назначаешь ей набор прав через визуальную сетку (видит лиды: да, видит финансы: нет, меняет настройки бота: нет), приглашаешь сотрудника по email, он принимает инвайт через специальную страницу, логинится и видит только то, что ему разрешено.
774 строки в бэкенде данных, 2233 строки в API (включая email-сервис для приглашений), 3292 строки на фронтенде. Два дня от первого коммита до полностью рабочей системы.

Меня лично зацепил момент, когда я протестировал это на себе. Создал роль «Стажёр», убрал все галочки кроме «Просмотр лидов» и залогинился под этим аккаунтом. Интерфейс изменился: исчезли разделы настроек, финансов, аналитики. Осталась только воронка с лидами. Такой Botseller я ещё не видел. Чистый, минималистичный, без лишнего шума.
Это был момент, когда я понял: ограничение доступа это не про безопасность. Это про фокус. Менеджер, который не видит двадцать пять вкладок, быстрее начинает работать с клиентами. Администратор, у которого нет кнопки «удалить бота», не нажмёт её случайно.
Ночные задачи: CRM-система задач за одну ночь

Четверг. 01:39 ночи. Коммит: «Система задач CRM + заметки + assigned_to для лидов». 23 файла, 1407 строк. Всё это между ужином и рассветом.
Почему ночью? Потому что днём шла война с рассылками и деплой партнёрской программы. Задачи я писал, когда всё остальное уже работало и можно было сосредоточиться.
Идея была простой: в CRM Botseller не было способа поставить задачу сотруднику. Лид пришёл, менеджер его обработал. А дальше? Перезвонить через два дня, отправить коммерческое предложение, проверить оплату. Всё это жило в голове менеджера или в стикерах на мониторе.
Новая система: задача привязана к лиду, у неё есть ответственный (assigned_to), дедлайн, статус и заметки. Интерфейс: Sheet-панель, которая выезжает сбоку, не уводя из воронки. Дважды за ту же ночь я переписал UI. Сначала сделал модальное окно, потом понял, что модалка отрывает от контекста, и переделал в Sheet.

Оглядываясь назад, вижу паттерн, который повторялся у нас много раз: первое решение UI всегда модалка. Правильное решение всегда Sheet. Модалка говорит: «Остановись, разберись с этим». Sheet говорит: «Вот контекст, работай». В CRM, где менеджер живёт в воронке, контекст важнее всего.
CommandFlow: редизайн, который выбросил тысячу строк
Параллельно с задачами и партнёркой шёл редизайн CommandFlow, системы мониторинга команды. До недели 135 это был набор отдельных страниц: миссии, агенты, чаты. Одиннадцать таблиц в базе, но на фронтенде разрозненные экраны без связи между ними.
За неделю мы переделали интерфейс в master-detail: список миссий слева, детали справа. Один экран вместо трёх. «Агенты» переименовали в «Персоны», потому что слово «агент» путало с AI-агентами, а система мониторила живых людей. 1078 строк удалили, 9 компонентов осталось.

И тут хочу сказать кое-что честное: 1078 строк, которые мы удалили, это код, который я же и написал двумя неделями раньше. Двести строк на компонент, три экрана вместо одного, модалки вместо inline-редактирования. Всё это казалось логичным, когда я писал. И всё оказалось лишним, когда я начал пользоваться.
Лучший редизайн тот, который удаляет код. Не добавляет новые фичи поверх старых, а убирает слои, которые мешают. Удалять свой собственный код тяжелее, чем чужой. Но это и есть взросление продукта.
Clubator: пробный период, который чуть не сломал подписки

Clubator, наш pet-проект, платформа клубов по подписке. На этой неделе он получил пробный период: «Попробуй бесплатно 7 дней». Звучит элементарно. Реализация оказалась минным полем.
Семнадцать коммитов за неделю. Триальная активация, задачи по расписанию (напомнить за день до конца, предложить подписку), условия для аудиторий (trial_available, trial_expired), интеграция с платёжной системой Prodamus.
Главная ловушка: что делать, если человек оплатил подписку во время пробного периода? Первая версия кода этого не учитывала. Trial_expired триггер срабатывал даже если человек уже заплатил. Пришлось добавить проверку trial_converted: если платёж прошёл, триал считается конвертированным, expire-триггер не срабатывает.
Потом иностранные карты. Prodamus для зарубежных карт использует другой эндпоинт, и формат ответа отличается. Пробный период с иностранной картой ломался на этапе «продлить подписку». Ещё семь файлов, ещё 302 строки.
Мне кажется, триал это та фича, которую каждый основатель считает простой, пока не реализует. «Просто дай доступ на 7 дней и заблокируй» — это одно предложение. А за ним: часовые пояса (когда заканчивается «седьмой день»?), конвертация (оплата во время триала), возврат (если вернул деньги, возвращается ли триал?), повторный триал (можно ли попробовать второй раз?), follow-up (что отправить за день до конца?). Каждый вопрос это ветвление кода и тестовый сценарий.
Зубные врачи и кастомные инструменты

Отдельная история недели: стоматологическая клиника, для которой мы написали кастомные инструменты AI-бота. Бот умеет проверять свободные слоты в медицинской информационной системе (МИС) и записывать пациентов на приём.
167 строк кода. 60 строк на описание инструментов для языковой модели, 108 строк на логику интеграции. Бот спрашивает «Когда хотите записаться?», проверяет доступность в МИС клиники и бронирует время.
Почему я упоминаю это отдельно? Потому что это первый случай, когда наш AI-бот не просто отвечает на вопросы, а совершает действие во внешней системе. Разница между «бот-советчик» и «бот-оператор» как между GPS-навигатором и автопилотом. Навигатор подсказывает. Автопилот рулит. О том, как устроены AI-боты для медицины, мы подробно писали в статье про чат-ботов для стоматологий.
Что ещё произошло за неделю
SiteBS и SEO. Десять коммитов: пилларные страницы для ключевых направлений (AI-продавец, чат-боты, медицина, недвижимость), кластеризация контента по топикам, старт документации на Nextra, 72 файла первой и второй волны. Структурированная разметка SiteNavigationElement и SearchAction для поисковиков.
CommandPulse. Четыре коммита: интеграция с VPS, пуш мониторинга чатов и лидов, автоматическая регистрация ботов при деплое. Маленький инструмент, который тихо делает свою работу.
Биллинг и часовые пояса. Десять файлов, 174 строки: утилита billing-datetime.ts, которая корректно отображает даты в таблицах биллинга. Звучит скучно, но попробуйте объяснить клиенту из Владивостока, почему его транзакция показывает вчерашнее число, когда он заплатил сегодня.
Фаундерский урок недели: масштаб требует защитных слоёв

Если из этой недели вынести одну мысль, она такая:
Когда продукт начинает расти, каждая «простая» функция превращается в систему. И между «простой» версией и «системной» лежит слой защиты, который ты либо строишь заранее, либо строишь в панике потом.
Рассылки казались простыми, пока не появились дубли. Партнёрская программа казалась «реферальной ссылкой», пока не понадобились ранги, комиссии и выплаты. Права доступа казались «галочкой в настройках», пока клиент не сказал, что его стажёр видит финансы. Пробный период казался «флагом в базе», пока не появились иностранные карты и конвертация во время триала.
Защитный слой это не перфекционизм и не over-engineering. Это конкретный чек-лист:
- Что если данные пришли дважды? (Идемпотентность)
- Что если регистр отличается? (Нормализация)
- Что если список пустой? (Валидация на входе)
- Что если действие уже произошло? (Проверка состояния)
- Что если пользователь не владелец? (Авторизация)
Пять вопросов. Если задать их ДО написания кода, каждый ответ это пять строк валидации. Если задать ПОСЛЕ продакшена, каждый ответ это тикет, хотфикс и извинение клиенту.
В ретроспективе недели 136 я рассказывал, как мы построили WhatsApp-канал за 48 часов. Неделя 135 это изнанка: что происходит, когда каналы и рассылки начинают реально использовать. Спринт закончен, разгребание только начинается.
FAQ
Что такое ретроспектива Botseller?
Серия «Ретроспектива», бортжурнал в прошлое: от настоящего к первому коммиту. Каждый выпуск разбирает одну неделю разработки с git-логами, решениями, ошибками и личными уроками основателя. Все выпуски собраны в категории ретроспектива. Текущий бортжурнал ведётся в категории бортовой журнал.
Как исправить дубли в массовых рассылках?
Три защитных слоя: дедупликация получателей по нормализованному идентификатору (case-insensitive), Redis-ключи для предотвращения повторной отправки и валидация входных данных (непустой список ботов, корректный список получателей). Каждый слой ловит свой класс ошибок.
Что такое RBAC и зачем он нужен в CRM?
RBAC (Role-Based Access Control), управление доступом на основе ролей. В CRM это значит: владелец создаёт роли (менеджер, администратор, стажёр), назначает каждой роли набор прав через визуальную сетку, приглашает сотрудников. Сотрудник видит только разрешённые разделы. Это и безопасность, и фокус: меньше вкладок, быстрее работа.
Как реализовать пробный период в SaaS без потери конверсий?
Минимальный чек-лист: отдельный статус trial (не boolean-флаг), проверка конвертации при expire (если оплатил во время триала, не блокировать), follow-up цепочка (напоминание за день до конца), обработка иностранных карт (если платёжная система использует разные эндпоинты), защита от повторного триала.
Что такое кастомные инструменты AI-бота?
Это возможность дать AI-боту доступ к внешним системам: проверить наличие товара, записать клиента на приём, создать заявку в CRM. Бот переходит от роли «советчик» к роли «оператор». Он не только отвечает на вопросы, но и выполняет действия. В Botseller инструменты описываются декларативно и подключаются к конкретному боту.
Как устроена партнёрская программа Botseller?
Три уровня: реферальная ссылка (процент от первого платежа), интеграторская модель (постоянная комиссия от клиентов) и White Label (платформа под своим брендом). Система автоматизирована: отслеживание конверсий через пиксель, автоматический расчёт комиссий, ранговая система по активным клиентам. Подробности на странице для партнёров.
Зачем переименовали «агентов» в «персоны» в CommandFlow?
«Агент» перегруженный термин: в контексте AI это языковая модель, в контексте мониторинга живой сотрудник. Чтобы убрать путаницу, мы переименовали: «персона» однозначно означает человека с профилем, задачами и правами доступа.



