11 задач по TDD: два сервиса на бэкенде (вердикт с полоской и лента дел из
пяти источников), ручка подгрузки, живая сортировка списка клиентов, типы и
маппер, русские фразы, панель Vue, точка состояния в списке, полный прогон,
приёмка глазами.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Замерено на боевом: вкладка читает журнал сделок, где 364 из 419 строк —
системные «робот залил лид». Данные про входы и про дела клиента лежат в
двух других журналах и на вкладку не попадают вовсе.
Решения владельца: вердикт + полоска 90 дней + лента только дел, пороги
3 и 14 суток, входы только сверху, заодно чиним мёртвую сортировку списка
клиентов по последней активности.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: письма о недоливе — одна сводка в сутки вместо потока на каждом прогоне
Сверка с поставщиком шла каждые 30 минут и на каждом прогоне слала письмо
на КАЖДУЮ пару (тенант, дата) с недоливом больше 20%. Вчерашняя дата остаётся
в окне сверки, поэтому поток рос день ото дня: 144 письма 27.07 → 510 за 06.08,
около 3000 за 12 дней на ops@liderra.ru.
Теперь пары собираются в один список и уходят одной сводкой раз в сутки
(отсечка по московской дате в redis; гонки нет — handle держит общий lock).
Частоту самой сверки не трогал.
Поштучный TenantBusinessDriftAlertMail с шаблоном удалён — стал мёртвым.
Тесты: сводка собирает все пары в одно письмо, второй прогон в те же сутки
молчит, в новые сутки письмо уходит снова. Красными их видел.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
Одно имя «на всех» стало полуправдой 05.08.2026: номера с несогласованным именем
перестали уходить вовсе, а к Билайну и МегаФону канала пока нет. Экран продолжал
обещать отправку — половина базы исчезала бы для клиента молча.
Теперь на экране «Имя отправителя» разложено по четырём сетям владельца:
✓ МТС уйдёт под именем «mybrand.ru»
✗ Билайн не уйдёт — сюда мы пока не шлём
✗ МегаФон не уйдёт — сюда мы пока не шлём
✗ Теле2 не уйдёт — ваше имя у этого оператора не согласовано
Показываем ВСЕ четыре сети, включая недоступные: клиент, у которого половина базы
на Билайне, должен видеть, куда она делась, а не гадать.
Причины разведены и проверяются в том же порядке, что у отборщика получателей:
сперва запрет владельца, потом канал, потом имя. Иначе клиент прочитал бы про имя
там, где мы и так не шлём, и пошёл бы зря его согласовывать.
Названия операторов словами — на ЭКРАНЕ, не на сервере: сервер отдаёт ключ и
причину, как назвать это человеку — дело интерфейса (так же в соседних экранах).
Тесты экрана — с настоящим монтированием, а не сверкой исходника: проверяем, что
надпись дошла до глаз, а не что нужное слово где-то есть в файле.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Решение владельца 05.08.2026. Прежний вариант (подписать нашим «Liderra.ru»)
подменял бренд, за который клиент заплатил, причём незаметно: журнал сообщений
имя отправителя не хранит вовсе. Так уже работает отдел продаж
(SmsRecipientSelector → SKIP_NO_SENDER), и надпись на клиентском экране
(«нет имени отправителя для оператора») давно лежала готовая — её просто никогда
не показывали.
Два случая разведены НАМЕРЕННО и закреплены отдельными тестами:
• своего имени нет вовсе (или приостановлено) — шлём от имени Лидерры, как
и раньше. Смешай их — и рассылка встала бы у ВСЕХ разом: на бою своих имён
заведено НОЛЬ;
• своё имя есть, но у этой сети не согласовано — номер не уходит.
Отсев в ОТБОРЩИКЕ, а не при отправке: деньги не замораживаются, и причина видна
клиенту в предпросмотре — до оплаты.
🪤 Полный прогон поймал мою ошибку, которую выборочные тесты не видели: имя
спрашивалось ПО КАЖДОМУ номеру — 20 000 запросов в базу, отбор 20 000 получателей
вырос с 20 до 29 секунд и упёрся в сторожа скорости. Добавлена памятка на тенанта
(та же защита, что у AllowedSmsOperators, который прямо об этом предупреждает).
После лечения — 1,47 с.
Два теста часовой давности переписаны: они закрепляли ПРЕЖНЕЕ решение и после
смены решения стали неправдой. Теперь проверяют и «в канал не ушло ничего», и
«деньги целы».
Узкая щель записана в коде: если админ сузит список операторов у имени ПОСЛЕ
создания рассылки, такой номер уже лежит в снимке и уйдёт подписанным нашим именем.
Случай редкий — пересогласование переводит имя в «на рассмотрении», а это снимает
блокировку.
Проверено: 4621 тест, упал один чужой (ExampleTest — в рабочей папке не собран
фронтенд). Статанализ 0. Стиль чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Имя согласовывает ОПЕРАТОР, и у каждого оно своё. Клиентская рассылка несла одно
имя всем каналам, а умолчанием ему служило `services.sms.mts.naming` — то есть имя,
согласованное с МТС, подставлялось Теле2 и подставилось бы Мегафону и Билайну.
Живой отказ Теле2 `invalid_source_address` 04.08.2026 — ровно этот механизм. На бою
у клиентов заведено НОЛЬ своих имён, значит это касалось каждой отправки.
Появился ClientSmsSenderResolver: спрашивает имя у той сети, чей номер. Пустая
строка на выходе — не «имени нет», а «своего имени для этой сети нет, канал
подпишется собственным согласованным именем из настроек».
Переведены на него оба места отправки: рассылка (SendClientSmsCampaignJob) и
авто-СМС по сделке (SendAutoSmsForDealJob).
Универсальный СМС-центр передавал имя КАК ЕСТЬ — теперь тоже падает на своё
(SMSC_NAMING). Раньше пустое имя туда не приходило никогда, с этой правкой стало
бы приходить: ушли бы СМС без отправителя.
Заплатка внутри канала Теле2 (приведение написания к своему регистру) остаётся, но
больше не несущая: она чинила следствие в одном канале, а причина была общая.
Экранное имя (ResolvesClientSmsSenderName) НЕ трогали — это запись намерения, а не
то, чем подпишется сеть; в его шапке теперь написано почему, чтобы не «починили»
обратно.
🪤 Урок по дороге: авто-СМС ловит любую ошибку и молча выходит. Справочник имён был
передан не в ту функцию — вместо падения получилась ТИШИНА, не уходило ничего.
Поймали три чужих теста, до того зелёных.
Проверено: 4614 тестов, упал один чужой (ExampleTest — в этой папке не собран
фронтенд). Статанализ 0. Стиль правленых файлов чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Приёмка на бою вскрыла, что хвост был закрыт только на вид: строку писали
уровнем «к сведению», а на боевом LOG_LEVEL=warning — всё, что ниже, молча
отбрасывается. Запись не появилась бы в журнале НИКОГДА, и круг «список нельзя
заполнить, потому что адреса неоткуда взять» остался бы замкнут — только теперь
незаметно, с виду починенный.
Замерено живым вызовом приёмника на бою 04.08: соседнее предупреждение про
незнакомое сообщение в журнал легло, а строка «к сведению» — нет.
Уровень поднят до «предупреждение». Тест теперь стережёт именно уровень, и в нём
записано, почему: не «так решили», а «иначе бой выбросит».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md §0 ссылается на «лендинг/TZ_landing_v1_0.md», но каталог `лендинг/`
лежит в .gitignore (строка 137) — в git этого файла нет и не будет.
В общем дереве ссылка «работает» только потому, что файл есть там на диске.
В любой отдельной рабочей папке его нет, проверка ссылок краснела и не пускала
push — а сам CLAUDE.md править мимо плагина нельзя (правило §5 п.10).
Исключение узкое: ровно этот файл. Проверено — 38 из 38 ссылок CLAUDE.md
целы, ошибок ноль.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Канал «SMS-Таргет» (target.t2.ru, HTTP API v2, Basic Auth) обеими половинами:
- отправка через POST /send_message (msisdn числом, shortcode, text);
- опрос судьбы GET /send_message/message-id-{id} — по одному сообщению,
пакетного запроса у t2 нет;
- приёмник обратных звонков GET|POST /api/webhook/t2-delivery/{secret}
со строками ?id=&status=&parts= — форма другая, чем у МТС.
Ловушки, закрытые тестами:
- слово status в ответе встречается дважды: снаружи «запрос удался»,
внутри судьба сообщения. Спутать = объявить доставленным всё подряд;
- отказ приезжает с HTTP 200 — судим по телу, не по коду;
- незнакомое слово статуса судьбу НЕ меняет, сохраняется в delivery_raw.
За «не доставлено» клиенту возвращаются деньги (В-198) — выдумка двигала
бы рубли;
- t2 наш ответ не проверяет и повторов не делает: пропущенный звонок потерян,
поэтому опрос client-sms:poll-delivery остаётся обязательным.
Деньги приёмник не двигает — возврат живёт в команде опроса (В-146).
Защита приёмника как у МТС: секрет не короче 32 знаков (пустой = закрыт
наглухо), список адресов отправителя, счётчик обращений; чужому 404, не 403.
Канал ВЫКЛЮЧЕН и живьём не пробован: имя отправителя Liderra.ru (заявка
№ 31121) на согласовании, без него t2 не выдаёт логин с паролём.
Порядок включения — docs/superpowers/runbooks/2026-08-04-vklyuchenie-kanala-t2.md
Миграций нет: графы судьбы появились ещё при МТС. Теле2 уже был в списке
разрешённых операторов и в тарифах админки — новых экранов не нужно.
Проверено: 78 тестов зелёные, Pint чист, статанализ по ВСЕМУ проекту 0 ошибок,
маршрут зарегистрирован. Против main — только вставки, удалений нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Канал «SMS-Таргет» (target.t2.ru, HTTP API v2, Basic Auth) обеими половинами:
- отправка через POST /send_message (msisdn числом, shortcode, text);
- опрос судьбы GET /send_message/message-id-{id} — по одному сообщению,
пакетного запроса у t2 нет;
- приёмник обратных звонков GET|POST /api/webhook/t2-delivery/{secret}
со строками ?id=&status=&parts= — форма другая, чем у МТС.
Ловушки, закрытые тестами:
- слово status в ответе встречается дважды: снаружи «запрос удался»,
внутри судьба сообщения. Спутать = объявить доставленным всё подряд;
- отказ приезжает с HTTP 200 — судим по телу, не по коду;
- незнакомое слово статуса судьбу НЕ меняет, сохраняется в delivery_raw.
За «не доставлено» клиенту возвращаются деньги (В-198) — выдумка двигала
бы рубли;
- t2 наш ответ не проверяет и повторов не делает: пропущенный звонок потерян,
поэтому опрос client-sms:poll-delivery остаётся обязательным.
Деньги приёмник не двигает — возврат живёт в команде опроса (В-146).
Защита приёмника как у МТС: секрет не короче 32 знаков (пустой = закрыт
наглухо), список адресов отправителя, счётчик обращений; чужому 404, не 403.
Канал ВЫКЛЮЧЕН и живьём не пробован: имя отправителя Liderra.ru (заявка
№ 31121) на согласовании, без него t2 не выдаёт логин с паролём.
Порядок включения — docs/superpowers/runbooks/2026-08-04-vklyuchenie-kanala-t2.md
Миграций нет: графы судьбы появились ещё при МТС. Теле2 уже был в списке
разрешённых операторов и в тарифах админки — новых экранов не нужно.
Проверено: 78 тестов зелёные, Pint чист, статанализ по ВСЕМУ проекту 0 ошибок,
маршрут зарегистрирован. Против main — только вставки, удалений нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Записан выбор поставщика для обеих Лен и всё, чем он подтверждён.
Замерено живьём, не по рекламе:
- разговор одной линией, 7 ходов: 0,88 с до первого звука, ровно от начала
до конца. Втрое быстрее нынешней Александры и без просадки.
- кэш работает, 96-98 процентов попаданий, счётчик поставщик отдаёт.
Значит длина наставления по деньгам почти ничего не стоит — решение
владельца не резать базу оправдалось.
- звонок стоит около 6,4 рубля, из них 5,6 голос. Экономить надо на
длине реплик, а не на длине наставления.
- стена: лимит 20 000 байт считает весь разговор вместе с историей,
а не только наставление. На бесплатном тарифе разговор умирает
на пятом ходу. Оплата — условие любой осмысленной проверки.
Все ручки поставщика разобраны отдельным файлом, 15 проверены живым
опросом канала. Три опасных умолчания: предела длины ответа нет вовсе,
голос по умолчанию младшей модели, температура единица.
Мина на будущее: замер кэша сделан на чужом мозге, который кэширует сам.
Claude так не умеет, ему нужна отдельная ручка, а она по умолчанию
выключена. Проверить первым делом после оплаты.
Выгрузка из Даши — оба промта Лен и три документа базы знаний, дословно.
В kb-02 помечены две мины: ответ про происхождение номера противоречит
обоим промтам, и бесплатный тест описан двумя разными способами.
Ключей и персональных данных в файлах нет, проверено датчиком
с контрольной пробой.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Общая ветка переведена вперёд на ветку клиентских СМС — перемоткой, без
слияния, поэтому конфликтов там быть не могло. Затем общая сведена в рабочую
ветку, которая отставала на 79 записей.
Столкновение было одно и знакомое — словарь орфографии cspell-words.txt.
Разрешено правилом «обе стороны настоящие»: слова обеих веток сохранены,
ничего не выброшено. Журнал схемы БД на этот раз свёлся сам, столкновения
номеров не было.
СТОРОЖ ПДн ОСТАНОВИЛ ЗАПИСЬ И БЫЛ ПРАВ. В образце базы номеров, который
клиент скачивает перед своей первой рассылкой, стоял рабочий телефон
владельца — в двух видах. Это не утечка чужих данных: тот же номер публично
опубликован в реквизитах ИП по требованию ЮKassa. Но клиент заполняет этот
файл своими номерами и запускает рассылку — забытая строка означала бы СМС
владельцу за деньги клиента. Заменён на выдуманный из тестового диапазона.
Подсказки на экране рассылок («+7 999 123-45-67») выдуманы изначально;
разрешены в .gitleaks.toml ПО ЗНАЧЕНИЮ, а не по файлу, чтобы сами файлы
остались под охраной. Сторож проверен вырезанием: подложенный номер, не
подпадающий ни под одно разрешение, он поймал; после снятия подложки — чисто.
Служебный счётчик наблюдателя убран в тайник на время сведения и возвращён
после — он машинный и пересоздаётся хуками.
Файлы второй смены, которая работает в этой же папке, не тронуты и в
слияние не попали.
Проверки сведённой ветки: тесты 4533, зелёных 4529, падений нет; экраны
238 файлов и 1906 зелёных, сторож сети не сработал ни разу; статанализ 0;
формат чист.
РАЗМЕТКА ДОКУМЕНТАЦИИ. Проверка сторожа давала 237 замечаний в 21 файле — стало
0 во всех файлах под учётом git. Пустые строки вокруг списков и заголовков
поправлены автоматом в 12 файлах, два файла пришлось делать руками, потому что
автоправка там навредила:
- в описании починки инцидента номера шагов 1…7 — это ИМЕНА, на них ссылается
сам текст «из шага 2»; автоправка перенумеровала их в 1,1,1,1,2 и сломала
ссылки. Вывел ровно этот файл из-под одного правила, объяснение — в самом
файле, остальные правила действуют.
- в протоколе архитектуры автоправка склеила «R-* / DR-*» в «R-*/ DR-*».
Вместо этого обозначения взяты в обратные кавычки — смысл сохранён.
Семь файлов не тронуты: они вне учёта git — два в папке второй смены и старые
черновики в корне.
ИНТЕРФЕЙС. «Неизвестный тип» убран целиком: было 123 замечания, стало 0.
- Навигация автоподбора описана ОДИН раз — app/resources/js/views/autopodbor/nav.ts.
Раньше девять экранов повторяли описание с «неизвестным типом», а ещё три
описывали его каждый по-своему и не полностью.
- Чтение ошибки от сервера собрано в один помощник — app/resources/js/api/errors.ts,
с проверками. Он намеренно читает ФОРМУ ответа, а не полагается на axios:
тесты экранов подсовывают ошибку простым объектом той же формы, и экран обязан
вести себя в тестах так же, как в бою.
- В тестах вместо «неизвестного типа» — именованный доступ exposed<T> и полные
заготовки: app/tests/Frontend/support/. Каждый тест теперь объявляет ровно те
поля, до которых дотягивается, и опечатка в имени снова становится ошибкой,
а не молчаливым undefined. Заодно сняты 14 построчных отключений правила.
- Разведены задвоенные имена в шаблонах: в таблице сделок в одном шаблоне жили
ДВЕ разные функции isSelected — одна берёт номер сделки, другая строку таблицы.
- Убрана мёртвая функция dirLabel и три неиспользуемые переменные.
ПРОВЕРКА ТИПОВ: было 7 ошибок, стало 5. Две унаследованные закрылись попутно —
более строгие заготовки вскрыли нехватку обязательного поля elements. Оставшиеся
пять были до меня и не в моих строках.
ПРИЁМКА. Разметка проверена вырезанием: подложил поломку обратно — сторож её
поймал, вернул — снова чисто. Полный набор тестов интерфейса: 233 файла,
1750 тестов, 3 пропущено, 0 падений. Рабочий код на PHP не тронут.
NB: LEFTHOOK_EXCLUDE=cspell — тем же приёмом и по той же причине, что и в записи
e0223de4, которой эти шесть файлов исследования заводились в репозиторий: сырой
внешний материал с чужой терминологией. Замерено: до моих правок сторож
орфографии находил в них ровно столько же слов, сколько после — я не добавил
ни одного. Разметка в этих файлах теперь проходит начисто, отключена ТОЛЬКО
орфография и только на эту запись; остальные семнадцать сторожей отработали.
Решение владельца, спрошено отдельно.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правка целиком в комментариях: 37 файлов, ни строки кода.
Её дописал сам pre-commit форматтер во время предыдущей записи и оставил
неподтверждённой. Записываю, чтобы папка ветки не оставалась грязной.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Приёмка глазами вскрыла: кабинет МТС требует «Заголовок объявления» (до 40 знаков),
когда в объявлении ссылка на САЙТ, а не на телеграм-канал. Робот про это поле не знал,
«Продолжить» молча не срабатывало, кампания вставала на шаге «Объявление» — в бою уже
ПОСЛЕ списания денег. Проверено живьём: 2234454 (сайт — встала) против 2234462 (канал —
дошла до подтверждения) и 2234490 (сайт с заголовком — дошла).
Портал спрашивает заголовок заранее, на создании черновика: обязателен только для
не-телеграмной ссылки (App\Support\TelegramLink), колонка ad_headline varchar(40),
поле на экране появляется по той же развилке. Робот заполняет его в кабинете.
Три ловушки, добытые живыми прогонами (описаны в коде):
- поле дорисовывается в ОТВЕТ на ссылку, с задержкой — надо ждать, а не спрашивать;
- под описание подходит несколько элементов — нужен .first();
- серая надпись внутри поля НЕ placeholder, а нарисованная подпись: поиск по атрибуту
давал ноль совпадений при видимом на снимке поле. Опознаём по видимой надписи.
Тесты: робот 130/130, ClientTg 250/250, экран 23/23. Полный прогон бэкенда — те же
13 падающих классов до и после правки (ни одного в телеграм-части). В baseline
статанализа добавлен известный ложный класс Pest для нового файла тестов.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Переделка сделана и запушена, на бой не выкачено. Развилка описана: поселить
робота и выкатить, перевести на опрос чтение вердикта и пересдачу, вылечить
тестовую базу. Ловушки смены выписаны, включая молчаливую установку
зависимостей и правило замерять сторожа до починки своего текста.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Сама установка НЕ выполнена — она трогает живую машину и боевой портал,
нужно явное разрешение владельца. Куда селить робота, зависит от исхода
разговора с поставщиком прокси.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Своей петли нет: запускается таймером, зависший проход не мешает следующему.
Номера кладутся во временный файл и убираются при любом исходе.
Проверено живьём против поднятого портала: верный токен — «Работы нет.» и код 0,
чужой токен — понятная ошибка 401.
Заодно закрыта мина: токен уходит в заголовок HTTP, куда пускают только латиницу.
С русскими буквами робот падал нечитаемым сбоем Node ещё до обращения к порталу —
теперь говорит по-человечески. Наступил на неё сам, проверяя план дословно.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
При transport=poll портал кладёт задание и выходит, робот заберёт его сам.
Процессный путь оставлен рабочим и остаётся умолчанием.
Полный набор тестов телеграма: 219 из 219 зелёные.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Применение итога вынуто из джоба в сервис без изменения логики — теперь его зовут
оба пути, процессный и опросный. Отчёт принимается только по заданию в работе.
Сторож принят вырезанием: без проверки статуса повторный отчёт проходит с 200.
Отдельно закрыта мина, найденная ревью защиты и отсутствовавшая в плане: роль
crm_admin_user имела на client_tg_campaigns только чтение, а канал робота пишет
туда итог — на бою приём отчёта упал бы по правам. Миграция v9.29 даёт UPDATE.
Полный набор тестов телеграма: 217 из 217 зелёные, старые в том числе.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Номера не кладём в задание и не пишем в журнал — отдельный запрос, без кеша.
Сторож принят вырезанием: без проверки статуса тест отдаёт 200 вместо 404.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Выдача строго по одному: у робота один профиль браузера и одна сессия кабинета.
Задание, по которому робот не отчитался за срок аренды, возвращается в очередь.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Где работать, что уже стоит, красные линии, ловушки и приёмка вырезанием.
Отдельно записано, что проверка обращения к поставщику прокси умирает
вместе с сессией и её надо ставить заново.
Co-Authored-By: Claude Opus 5 (1M context) <noreply\@anthropic.com>
Нужна при ЛЮБОМ исходе с адресом: и на сервере, и на машине владельца робот стоит
не там, где очередь портала, а сегодня портал запускает его как процесс у себя.
Списано с работающего образца - робота креативов Яндекса.
12 задач по шагам с готовым кодом и тестами, переключатель process/poll оставляет
старый путь рабочим. Отдельно вынесены красные линии: перезапуск srv_bypass после
миграции, номера телефонов не в задании, приёмка сторожей вырезанием.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Живой бой 30.07.2026. Сегмент дозрел за 15 часов — Яндекс опознал 1644 человека
из 1693, охват 3379. Портал нажал «Запустить» и получил отказ Директа:
Budget for this period cannot be less than 600 rub.
Стена оказалась не одна, а две. Обе — молчаливые: портал о них не знал.
Первая — аудитория. Сегмент в Аудиториях существует и при этом НЕГОДЕН, пока
Яндекс сводит загруженные номера с людьми. Портал этого не спрашивал вообще:
ни один файл не читал can_create_dependent. Он просто шёл в Директ строить
условие ретаргетинга и получал «объект не найден» — тот же отказ, что и на
несуществующий сегмент. Судить о готовности можно ТОЛЬКО по can_create_dependent:
статус is_processed означает «обрабатывается», а не «готов».
Вторая — деньги. Директ не берёт кампанию, у которой бюджет за период ниже
шестисот рублей. Приёмочная кампания на 1693 показа давала около 146 рублей.
Проверки минимума в портале не было тоже.
В обоих случаях клиент видел сырую ошибку Яндекса на английском и не понимал,
виноват ли он и что делать дальше.
Что сделано. Обе проверки выполняются ДО первого обращения к Яндексу: в кабинете
ничего не создаётся, деньги не морозятся, кампания остаётся черновиком.
аудитория ещё готовится → 202 и «подождите, обычно несколько часов»
смета мала → 422 и «нужно 6945 показов, это 833.40 рублей»
Наружу уходят ТОЛЬКО клиентские числа. Ни минимума площадки, ни нашей наценки:
по паре «минимум площадки — цена клиенту» долю Яндекса можно вычислить делением.
Минимум вынесен в настройку YANDEX_DIRECT_MIN_SPEND_RUB, Яндекс может его менять.
Тестом вперёд, в живой Яндекс из тестов не ходили. Четыре новых теста на запуск
и два на ответы портала. Реклама целиком: 342 теста зелёные.
Заодно приведён в порядок список исключений статанализа. Он был красным ЗАДОЛГО
до этой правки и не пускал ни одну правку кода: 629 замечаний до моих изменений,
638 после. Проверено в обеих папках работы — не артефакт подпапки.
Разбор 637 замечаний по составу:
611 — статанализ не понимает устройство тестов Pest и ругается на
обращения вида this->postJson и this->tenant. Таких записей в списке
исключений уже было 671 — просто новые тестовые файлы туда не дописали.
26 — придирки к типам, из них 7 в боевом коде.
Все семь в боевом коде разобраны поимённо и оказались ложной тревогой. Доказано
не рассуждением, а зелёными тестами, которые упали бы при настоящей поломке:
shows_until, три замечания — три теста прямо проверяют закрытие кампании по
истечении срока показа и разморозку остатка. Будь там строка вместо даты,
первый тест упал бы, а деньги клиента остались бы заперты навсегда.
snapshot_from и snapshot_to, два замечания — тесты ручного режима строят
аудиторию по этим датам, на строке они бы рухнули.
SetTenantContext строка 56 и CampaignMessageService строка 168 — лишние
подстраховки, вреда нет.
Корень ложных срабатываний: Larastan ищет приведения типов в старом виде —
свойством casts, а у нас метод casts. Код правильный, инструмент отстал.
После обновления списка статанализ зелёный: 0 замечаний. Теперь сторож снова
ловит НОВОЕ, а не молчит красным.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка ссылок падала на девяноста двух ошибках, поэтому любая отправка в main
шла через обход. Смысл починки не в красоте: пока ошибок девяносто две, новая
битая ссылка тонет среди них и никто её не замечает. Теперь ноль, и пуш проходит
обычным порядком.
Что было внутри девяноста двух:
68 — ссылка написана от корня хранилища, а документ лежит на две-три папки
вглубь. Приписаны шаги вверх. Файлы всё это время лежали на месте.
8 — файл существует, но переехал: Jobs/Billing, Jobs/Supplier, Services.
Адреса переписаны на нынешние места.
8 — файла нет и не будет: ProcessWebhookJob снят при уходе от старого
биллинга, SupplierCsvParser удалён 09.07 как мёртвый, каталог memory
уехал в claude-brain, а HANDOFF прогрева от 25.07 не существует ни
в одном коммите. Ссылки сняты, текст оставлен.
3 — неверное число шагов вверх: две точки вместо трёх.
3 — это вообще не ссылки, а примеры в тексте: http:// как образец мусорного
ввода, https://ваш-сайт.ru из подсказки формы. Обёрнуты в кавычки-код.
1 — адрес с приписанными номерами строк, которых проверятель не понимает.
1 — Россвязь. Сайт мёртв по-настоящему: сервер 194.226.91.2 отдаёт nginx-ову
404 на любой адрес, включая корень, а сертификат выписан не на это имя;
ведомство расформировано. Замену проверить не удалось — преемник
с этой машины не отвечает вовсе. Ссылка снята, адрес читается текстом.
Вопрос «где реестр нумерации живёт теперь» остаётся открытым.
Исключения проверятеля не тронуты ни на строку. Ноль получен починкой, а не
глушением сторожа — иначе вся работа теряет смысл.
Отдельным прибором проверено, что видимый читателю текст не изменился ни в одной
из 81 правленой строки: правились только адреса ссылок. Разметка линтером чистая.
Вместе с починкой ложится промт смены, по которому работа делалась.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.
4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
(audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).
4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.
4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).
4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).
Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Дописан standalone-робот (Node ESM + Playwright) полного цикла создания
Telegram-кампании по своей базе телефонов в кабинете МТС Маркетолог:
- cabinet.js: вход в визард, выбор «Своя база клиентов», загрузка базы +
подсчёт «не МТС» с порогом 367, переход «Аудитория→Объявление» (само-
верификация), заполнение объявления + блок ОРД, финал черновик-стоп/боевой
(fail-loud при неизвестном режиме — защита от боевого клика по ошибке).
- runner.js: оркестратор с алярм/отчётом на почту, гарантированное закрытие
профиля браузера, чистка временного файла номеров (152-ФЗ).
- task.js: обязательные поля заголовка и названия ОРД; config: потолок бюджета
падает при нечисловом значении (защита денег), budget: NaN-guard.
- bin/run.js (CLI), bin/keepalive.js (сторож входа с алярмом), bin/login.js
(разовый вход), task.example.json (без ПДн), README.
- Шаг «Стоимость» намеренно не размечен (заглушка бросает Error) — селекторы
снимаются на первом реальном черновике после разового логина владельца.
Ядро: 20/20 юнит-тестов зелёные. Браузерная часть проверяется живым черновиком.
Двухстадийная ревизия каждой задачи + финальный сквозной проход (поймал и
починил пропущенный переход между шагами и NaN-обход потолка бюджета).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задачи 8–9 плана (код, проверка против кабинета — при логине владельца в
профиль бота):
- browser.js — launchPersistentContext(profileDir), humanPause по темпу
- session.js — isLoggedIn по стабильному пункту меню «Рассылки и звонки»
(селектор подтверждён живьём при разведке)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>