Мой коммит уехал в main отдельно — пересадкой на актуальную вершину
и оттуда на боевой. Возвращаю main к себе, чтобы ветка не разъезжалась.
Пришло 9 коммитов, из них главное — работа соседней сессии по воронке
продаж, уже влитая в main и запушенная.
Конфликта два, оба в общих машинных файлах:
- cspell-words.txt — сторону не выбирал, объединил. Все слова обеих
сторон на месте; убрана ровно одна строка — мой же дубль «админский»,
который прошлый коммит добавил дважды. Проверено сравнением с версией
до слияния: другого отличия нет;
- docs/observer/STATUS.md — машинный файл наблюдателя со столбиком часов
процессов, взята своя версия, его всё равно переписывает хук.
Замечание на будущее: патч «минимум площадки считается по длине периода»
живёт в двух коммитах — 948807507 у меня и в ветке робота, 9df7846fd
в main. В main уехала только вторая копия, main чист. Когда моя ветка
и ветка робота пойдут в main, git встретит этот патч второй раз.
Статанализ остановил слияние, и это была НЕ ложная тревога: файл-эталон
phpstan-baseline.neon склеился из двух версий и перестал соответствовать
коду. Все 16 ошибок — в чужих файлах воронки продаж, пришедших из main
байт в байт, известного ложного класса про Pest. Эталон из main один
не подошёл: всплыли записи, нужные этой ветке. Пересобрал по проектной
процедуре в два шага, phpstan.neon вернул на место. Итог 0 ошибок,
правка эталона мелкая: +43 -31. Моих файлов среди добавленных записей
нет ни одного.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кампания 713175197 третьи сутки не показывалась при замороженных у клиента
деньгах, а программный интерфейс на всех уровнях рапортовал «Идут показы».
Правду отдала только подпись на экране кабинета: «Для показа в заданных
регионах предоставьте документы» — 13 объявлений придержаны, 2 отклонены.
1. Яндекс отвечает машине ПО-АНГЛИЙСКИ, если не попросить русский.
Замер боевым ключом, два одинаковых запроса подряд: без заголовка
«Rejected at moderation.», с Accept-Language ru «Отклонено на модерации.».
Английская строка уезжала клиенту в переписку и письмом как пояснение
модератора, честная заглушка «причину выясняем» не срабатывала никогда,
а на следующем обходе английская строка ложилась ПОВЕРХ доклада разведчика
— проверено по боевой ленте: 30.07 в 15:02 робот принёс полную причину,
в 17:00 её накрыло. Лечение: спрашиваем язык явно плюс второй заслон —
английские отписки узнаются как отписки.
2. Отказ включения Яндекс кладёт в Warnings, а код смотрел только в Errors.
Ответ целиком: ResumeResults с Warnings 10201 «Объявление не остановлено»
при пустом Errors. Исключения нет, портал считал, что справился: двое суток
обход каждые два часа поднимал 13 объявлений, ноль записей в журнале.
Лечение: resumeAds возвращает, кого включить не дали.
3. Новый исход «принято, но придержано» порталу не был известен вовсе.
Вердикт ACCEPTED, ярлыка «Отклонено» нет, разведчик не ходил — клиент видел
«Идут показы» при нулевых показах. Теперь по отказу включения клиенту идёт
сообщение «Яндекс принял объявление, но пока не показывает его. Причину
выясняем» — текст выбран владельцем — и туда же едет разведка.
Ловушка, обойденная по дороге: включение спрашиваем ДО разбора объявлений.
Наоборот — сообщение о придержке легло бы поверх доклада разведчика, и обход
начал бы чередовать их по кругу: защита от дублей смотрит на последнее сообщение.
Замеры. Восемь новых сторожей, все написаны ДО починки и падали именно
на живых данных. Отдельный сторож на противоположный случай — включённому
объявлению разведку не заводим — был зелёным с самого начала.
Портал 4083 теста, 4043 прошло, 16 упало: те же шесть давних классов и ровно
те же числа, что до работы, было 4069/4029/16. Плюс 14 тестов — ровно новые.
Pint и Larastan по изменённым файлам чистые.
Главный урок записан в cabinet-flow.md §7.9: в §7.7 лежит правдивый РУССКИЙ
ответ Яндекса, снятый ДРУГИМ инструментом, который язык просил. Разбор отписок
построили по показаниям прибора, которым продукт не пользуется. Мерить надо той
же дорогой, по которой ходит боевой код.
На боевой НЕ выкатывалось.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Кампания 713175197 третьи сутки не показывалась при замороженных у клиента
деньгах, а программный интерфейс на всех уровнях рапортовал «Идут показы».
Правду отдала только подпись на экране кабинета: «Для показа в заданных
регионах предоставьте документы» — 13 объявлений придержаны, 2 отклонены.
1. Яндекс отвечает машине ПО-АНГЛИЙСКИ, если не попросить русский.
Замер боевым ключом, два одинаковых запроса подряд: без заголовка
«Rejected at moderation.», с Accept-Language ru «Отклонено на модерации.».
Английская строка уезжала клиенту в переписку и письмом как пояснение
модератора, честная заглушка «причину выясняем» не срабатывала никогда,
а на следующем обходе английская строка ложилась ПОВЕРХ доклада разведчика
— проверено по боевой ленте: 30.07 в 15:02 робот принёс полную причину,
в 17:00 её накрыло. Лечение: спрашиваем язык явно плюс второй заслон —
английские отписки узнаются как отписки.
2. Отказ включения Яндекс кладёт в Warnings, а код смотрел только в Errors.
Ответ целиком: ResumeResults с Warnings 10201 «Объявление не остановлено»
при пустом Errors. Исключения нет, портал считал, что справился: двое суток
обход каждые два часа поднимал 13 объявлений, ноль записей в журнале.
Лечение: resumeAds возвращает, кого включить не дали.
3. Новый исход «принято, но придержано» порталу не был известен вовсе.
Вердикт ACCEPTED, ярлыка «Отклонено» нет, разведчик не ходил — клиент видел
«Идут показы» при нулевых показах. Теперь по отказу включения клиенту идёт
сообщение «Яндекс принял объявление, но пока не показывает его. Причину
выясняем» — текст выбран владельцем — и туда же едет разведка.
Ловушка, обойденная по дороге: включение спрашиваем ДО разбора объявлений.
Наоборот — сообщение о придержке легло бы поверх доклада разведчика, и обход
начал бы чередовать их по кругу: защита от дублей смотрит на последнее сообщение.
Замеры. Восемь новых сторожей, все написаны ДО починки и падали именно
на живых данных. Отдельный сторож на противоположный случай — включённому
объявлению разведку не заводим — был зелёным с самого начала.
Портал 4083 теста, 4043 прошло, 16 упало: те же шесть давних классов и ровно
те же числа, что до работы, было 4069/4029/16. Плюс 14 тестов — ровно новые.
Pint и Larastan по изменённым файлам чистые.
Главный урок записан в cabinet-flow.md §7.9: в §7.7 лежит правдивый РУССКИЙ
ответ Яндекса, снятый ДРУГИМ инструментом, который язык просил. Разбор отписок
построили по показаниям прибора, которым продукт не пользуется. Мерить надо той
же дорогой, по которой ходит боевой код.
На боевой НЕ выкатывалось.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
По правке владельца со скрина: блок фильтра по датам переехал из отдельной
строки под заголовком в общий ряд справа — к «Менеджер» и «Происхождение».
Порядок внутри блока перевёрнут: САМ ФИЛЬТР крайний справа, СРОК левее него.
Читается справа налево: «что менялось» → «вчера».
Ряд получил flex-wrap: на узком экране переносится, а не уезжает за край.
На экране менеджера тот же ряд — экраны не должны разъезжаться.
Порядок закреплён тестом, иначе его снова переставят.
Перед слиянием с main я откатил 4 файла к своей старой версии, чтобы
сдвинуть с места застрявшее слияние — и слияние закрепило этот откат.
Пострадала починка от 29.07: чтение связок поставщика из pivot (без неё
5 из 9 работающих проектов показывали жёлтое «Готовим к запуску» при
живом заказе) и два набора тестов вебхука/сверки CSV.
Файлы возвращены к состоянию main 7361d1ae3. На боевой откат НЕ уезжал —
выкат был точечный, только 6 файлов воронки продаж; проверено на живом
сервере: починка там на месте.
- 12-я колонка «Корзина», результат «В корзину» просит только причину;
- в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного
тестирования»; при нуле дробь не рисуется — «69/0» это шум;
- у «Выслано КП» поле даты подписано «если договорились» и необязательно;
- орган фильтра по датам на ОБОИХ экранах, два режима + период.
Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела»
(снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/).
Браузер поймал то, чего не видели тесты: при смене режима оставался прежний
период, и «что менялось за завтра» давало пустую доску — теперь период
возвращается к «Сегодня», на это заведён отдельный тест.
Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31.
Влил fix/robot-yandex-zamok (26 коммитов: сведение с основной, стадии воронки
«Тестирование ручное» и «Выслано КП», чтение вердикта и пересдача по опросу)
в ветку заголовка объявления.
Конфликт был один — docs/observer/STATUS.md, машинный файл наблюдателя
со столбиком часов процессов. Взята своя, более свежая версия; файл всё равно
перезаписывается хуком.
Замер после слияния (своя тестовая база liderra_testing_zag, прогон в тишине):
портал 4069 тестов, 4029 прошло, 16 упало; робот 130/130.
До слияния было 4063/4018/17. Тестов больше, падений меньше — стык чистый.
Падают шесть давних классов, не связанных с этой работой: CreativeRobotEndpoint
(11 — лезет в живой Директ и получает «недействительный ключ»), CreativeJobService,
InAppNotification, PhoneRegionSmoke, ProjectExtensions, SalesOverview.
Проверено отдельно: таблица заданий роботу не ограничивает список режимов
(обычная строка), поэтому новые «read-status» и «resubmit» миграции не требуют.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- результат «В корзину»: одна причина, дата созвона стирается, платящего не выбросить;
- реклама на корзине встаёт СРАЗУ — явной веткой, а не случайно через unknown_stage;
- у «Выслано КП» дата созвона стала необязательной: бывает «пришлите на почту,
если интересно — перезвоню». Врущий старый тест на 422 без даты поправлен;
- фильтр доски date_mode=todo|changed + период (добавлен вид «завтра»).
«Надо сделать» — созвон в периоде ИЛИ просрочен, кроме отказа и корзины.
«Менялось» — есть движение стадии ИЛИ запись разговора за период.
Прогон: 1245/1245 (отдел продаж + все юнит-тесты).
Стадию меняют два разных места — результат разговора менеджера и автопереезд
по деньгам. Джоба не оставляла следа вообще, поэтому «что менялось за день»
было не из чего построить. Запись движения перенесена в событие модели: один
шов на всех, включая любой будущий третий источник.
- sales_prospect_moves — журнал всех движений (append-only, с GRANT'ами ролям);
- prev_stage на карточке — откуда приехала (кормит счётчик «69/1» у «Отказа»);
- stage += 'trash' — место под колонку «Корзина».
Сторож проверен вырезанием: без записи движения краснеют 4 теста, в том числе
тест джобы и тест API.
Приёмка глазами вскрыла: кабинет МТС требует «Заголовок объявления» (до 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>
Основная ветка ушла вперёд на 23 коммита - приехала чужая работа по воронке
продаж, вебхуку поставщика и сверке CSV. Конфликтов было два.
1. Журнал схемы db/CHANGELOG_schema.md. В обеих ветках лежала запись v9.28
от 31.07 про разное: у нас очередь заданий роботу, у них стадии воронки
"Тестирование ручное" и "Выслано КП". Обе записи настоящие, поэтому ни одна
не выброшена: боевая v9.28 осталась на своём номере, наши две подвинуты на
v9.29 очередь заданий и v9.30 грант админ-роли. Поправлены ссылки на номер
в шапках двух миграций, перекрёстные ссылки внутри самих записей и врезка
"Перенумерация" вверху журнала - там теперь описан и этот случай.
2. docs/observer/STATUS.md - файл авто-генерируемый, взята версия основной
ветки, хук перепишет его сам.
Заголовок db/schema.sql не трогали: там своя нумерация v8.85, ни одна из
веток её не двигала.
Плюс одна настоящая находка статанализа, приехавшая с чужой работой: у метода
SupplierPortalClient::fetchDeliveredLeads в описании возвращаемого набора не
было поля tag, хотя код его уже возвращает и чужой же тест его ждёт. Дописал
одно поле в описание, логику не трогал. Список исключений статанализа пересобран
- разъехались счётчики ложного класса Pest от новых строк в чужих тестах.
Проверено на ОТДЕЛЬНОЙ тестовой базе liderra_testing_tgmerge:
телеграм на портале 244 из 244
робот 124 из 124
статанализ 0 замечаний
код-стиль чисто
полный прогон 4063 теста, 4018 прошло, 17 упало
До сведения было 4039 тестов и 20 падений - тестов стало больше, падений
меньше. Ни одно падение не касается телеграма или журнала схемы: 13 из 17 в
рекламном модуле Яндекса, из них 11 - живой отказ авторизации Яндекс Директа,
остальные счётные, от накопленных за прогон данных.
Отдельно вскрылось при проверке: общая тестовая база liderra_testing испорчена -
в ней 180 записей о применённых миграциях при 146 файлах, то есть в неё пишет
не только эта рабочая папка. Из-за этого сборка базы срывалась на первом же
шаге. Отдельная база всё вылечила. Это хвост Х2б, лечение в репозиторий не
вносил - решение владельца.
На бой ничего не выкачено, переключатель TG_ROBOT_TRANSPORT остаётся в process.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Теперь на опрос переведены все три работы робота, а не только запуск кампании.
Портал кладёт задание в таблицу, робот сам приходит за ним и отчитывается.
Это нужно потому, что робот живёт не на машине очереди - МТС не пускает адреса
дата-центров.
Что сделано по плану docs/superpowers/plans/2026-07-31-tg-poll-read-status-resubmit.md:
- два новых режима задания: чтение вердикта модерации и пересдача;
- применение вердикта вынуто из джоба в TelegramModerationVerdictApplier,
применение итога пересдачи - в TelegramResubmitResultApplier; оба переноса
построчные, денежная логика не менялась ни в одном символе;
- у обоих джобов появилась развилка по каналу: на опросе задание ставится,
робот не запускается;
- приёмщик отчёта различает режимы - иначе отчёт о чтении вердикта применился
бы как отчёт о запуске и сдвинул кампанию не туда;
- номера телефонов больше не выдаются режимам, которым они не нужны: чтению
вердикта и пересдаче аудитория не требуется, она у кампании уже есть;
- у робота развилка по режиму вынесена в отдельный src/poll-plan.js, чтобы
её можно было проверять без браузера и без сети;
- сторож прав на бою: роль портала обязана иметь право ставить задания.
Новых миграций и новых прав НЕ понадобилось: все три места ставят задание под
подключением по умолчанию, то есть под ролью портала, у которой права уже есть.
Схема БД не менялась, запись в CHANGELOG не требуется.
Приёмка вырезанием: убираем ограничение по режиму в выдаче номеров - два теста
падают, возвращаем - зелёные.
Проверено:
телеграм на портале 244 из 244 (было 219, старые тесты в том числе)
робот 124 из 124 (было 120)
статанализ 0 настоящих замечаний
полный прогон 4039 тестов, 3995 прошло, 20 упало
Из 20 падений 19 - те же давние, что были до работы. Двадцатое -
ExternalServiceDownAlertTest, в одиночку проходит 3 из 3 и вместе с телеграм-
тестами тоже; падает только в полном прогоне от накопленных данных. Это
известная слабость: у большинства файлов нет изоляции между тестами.
Заодно сборка тестовой БД переведена с migrate:fresh на связку
db:wipe --drop-types + migrate: первая спотыкалась на призрачном типе
legal_entities, вторая на тех же состояниях отрабатывала без отказов.
На бой не выкачено, переключатель TG_ROBOT_TRANSPORT остаётся в process.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Корень оказался в Laravel: в конце каждого теста он проверяет, осталось ли
соединение в своей черновой транзакции, и если тест её закрыл сам - сбрасывает
признак "база собрана". Следующий тест пересобирает базу целиком прямо посреди
прогона, снося её под всеми остальными. В проекте полно кода со своими
транзакциями, поэтому срабатывало примерно раз на восемь прогонов.
Что сделано:
- пересборка базы теперь один раз и в самом начале прогона, а не лениво в
середине по первому файлу, который её закажет;
- признак "собрано" держится взведённым - пересборка посреди прогона стала
невозможна;
- отказ заливки схемы стал громким: раньше PDO мог вернуть отказ без ошибки,
и прогон ехал дальше по неполной схеме;
- добавлена сверка полноты сборки: сколько шагов записано против того,
сколько их лежит.
Приёмка вырезанием: новый тест-сторож зелёный с защитой и падает без неё,
прогон без защиты дольше на 12 секунд - это и есть лишняя пересборка.
Замер до и после, полный набор портала:
без правки 3175 прошло, 837 упало
с правкой, прогон 1 3971 прошло, 19 упало
с правкой, прогон 2 3971 прошло, 19 упало, списки совпали дословно
Оставшиеся 19 - давние и не от базы: 11 падают на неверном токене Яндекса,
одна на типе исключения, семь счётных от накопления данных между тестами.
Плюс план перевода чтения вердикта и пересдачи на опрос - отдельным файлом,
код по нему ещё не писался.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Конфликты только в документах: журнал «мозга» взят из main, запись v9.28
переставлена поверх боевого журнала схемы. Код не пересекался.
Сторож слоёв проверен отдельно в рабочей папке: 0 нарушений.
Сторож секретов gitleaks отработал по-настоящему.
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>
До правки decide на незнакомой стадии возвращал unknown_stage — реклама на
такие фирмы встала бы без единого сообщения. Теперь обе новые стадии идут
по правилу переговоров: крутим до созвона, потом ещё срок просрочки.
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>
Телефон приводится к виду 79… без плюса — как контакты карточки и как
принимают Яндекс Аудитории. Неразобранный номер отказывает, карточку не двигает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Список стадий правится в двух местах сразу: PROSPECT_STAGES на фронте и
STAGES в контроллере. Расхождение дало бы пустую колонку.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
У объявления два независимых признака: вердикт модерации (Status) и идёт ли
показ (State). Созданное программой объявление рождается выключенным, и
включение кампании его не поднимает — команды поднять сами объявления в коде
не было вообще.
Живая приёмка 31.07.2026, кампания 713175197: принята и включена, Яндекс на
уровне кампании пишет «Идут показы», а внутри все 15 объявлений выключены.
Показов ноль при замороженных у клиента 3333.36 руб.
Опросчик теперь чинит по СОСТОЯНИЮ из ответа Яндекса, а не по переходу
статуса: собирает объявления со статусом «принято» и выключенным показом и
зовёт ads.resume. Так самовосстанавливается и уже запущенная кампания — по
переходу она осталась бы выключенной навсегда, потому что «принято» у нас
записано давно. Состояние Яндекс присылал в каждом ответе и раньше, опросчик
его просто выбрасывал.
Проверено вырезанием: без починки новый тест краснеет. Реклама 349 из 349.
Живой запуск кампании на бою 30.07.2026 вскрыл две поломки, которых вчерашняя
починка не видела. Обе — из класса молчаливых: портал рапортует успех, а в жизни
ничего не происходит.
ПЕРВАЯ. Минимум Директа НЕ постоянный.
Вчера мы приняли живой отказ «Budget for this period cannot be less than 600 rub.»
за постоянный порог и зашили 600 ₽ в настройку. Сегодня та же кампания на неделю
получила отказ «cannot be less than 2400 rub.» — проверка её пропустила, и клиент
снова увидел английский текст.
Две точки дали правило: минимум = 300 ₽ за каждый календарный день периода,
считая оба края.
период 30.07-31.07, 2 дня → 600 ₽ = 2 × 300
период 30.07-06.08, 8 дней → 2400 ₽ = 8 × 300
Теперь порог считается от периода показа, а ставка за день вынесена в настройку
YANDEX_DIRECT_MIN_SPEND_RUB_PER_DAY. Период вычисляется ДО денег — иначе считать
минимум не от чего.
ВТОРАЯ. Создать объявления — не значит запустить рекламу.
Запуск проходил успешно, портал ставил статус «на модерации», а в кабинете лежали
кампания, группа и 15 объявлений в состоянии DRAFT и OFF. Яндекс кладёт всё
созданное черновиком и проверку сам не начинает. Реклама не показалась бы никогда,
и узнать об этом можно было только глазами в кабинете: наш журнал говорил, что всё
хорошо.
Добавлены два вызова, которых не было вовсе:
ads.moderate — отдать созданные объявления на проверку
campaigns.resume — включить показ
Порядок обязателен. Пока кампания черновик, включить её нельзя — Директ отвечает
«кампания является черновиком и не может быть остановлена». Сначала модерация,
она переводит кампанию в MODERATION, и только потом включение.
Отбор объявлений строго по Ids. На CampaignIds Директ отвечает «отсутствует
обязательный параметр Ids» — именно так отправка молча не сработала бы.
На модерацию уходят объявления, созданные ИМЕННО этим заходом: повторная отправка
уже проверяемого — отказ по позиции, он порвал бы возобновляемый запуск. Включение
показа безобидно при любом повторе: на включённой кампании Яндекс отвечает
предупреждением, а не отказом.
Заодно клиент Директа перестал молчать про отказы внутри ответа: раньше он смотрел
только AddResults, UpdateResults и DeleteResults, теперь ещё ModerateResults и
ResumeResults. Отказ по позиции в этих двух проходил бы насквозь незамеченным.
ПРОВЕРЕНО ЖИВЬЁМ. Кампания № 6 на бою: в кабинете 713175197, группа 5778555449,
15 объявлений, аудитория подключена, статус MODERATION, показ включён. На счету
Яндекса 8775 ₽. У клиента заморожено 3333.36 ₽ за 27778 показов до 06.08.
Тесты: 346 зелёных по рекламе, 43 по клиенту Директа, статанализ ноль замечаний.
Четыре новых теста — минимум по длине периода на боевом случае, отправка на
модерацию, порядок модерация-до-включения, отсутствие повторной отправки.
Два старых теста закрепляли частный случай в два дня — им проставлен явный срок
показа, иначе они молча описывали бы неправду.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Продолжение разбора 31.07.2026. У пяти сегодняшних добранных сделок (223, 225-228)
пусты регион и город. Проверил три источника: ДаData по этим номерам молчит
(qc=0, виртуальные операторы МиАТел/МТТ/Скартел/ВымпелКом), Россвязь не совпала,
тег поставщика у них — «РФ», то есть вся страна. Восстановить их регион НЕЧЕМ,
задним числом не чиню и не выдумываю.
Но причина на будущее устранима: в журнале отданного ЕСТЬ колонка «Тег», и там
бывает настоящий регион (у одного из семи сегодняшних — «Свердловская область»).
Мы её просто не читали, поэтому у добранного лида в карточке было только
vid+phone+project, и когда ДаData молчит, резолверу нечем подстраховаться —
RegionTagResolver работает именно по тегу.
- parseDeliveredRows: тег берётся из ячейки сразу после td.crm-domain-column.
Разметка списана с ЖИВОГО кабинета, не придумана. Прочерк/пусто → tag=null,
поведение как раньше (парсер этого журнала уже дважды ломал прод — 09.07 и 16.07,
поэтому только добавление поля, ни одна существующая ветка не тронута).
- CsvReconcileJob: тег кладётся в raw_payload добранного лида.
Тесты (20/20 в файле, 233/233 поставщик+вебхук): разбор живой разметки, «РФ» и
прочерк, тег доезжает в карточку и — главное — с тегом резолвер даёт регион
(source=tag), а стоит тег вырезать, тот же лид снова «регион неизвестен».
Larastan 0, Pint чисто.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
У объявления два независимых признака: вердикт модерации (Status) и идёт ли
показ (State). Созданное программой объявление рождается выключенным, и
включение кампании его не поднимает — команды поднять сами объявления в коде
не было вообще.
Живая приёмка 31.07.2026, кампания 713175197: принята и включена, Яндекс на
уровне кампании пишет «Идут показы», а внутри все 15 объявлений выключены.
Показов ноль при замороженных у клиента 3333.36 руб.
Опросчик теперь чинит по СОСТОЯНИЮ из ответа Яндекса, а не по переходу
статуса: собирает объявления со статусом «принято» и выключенным показом и
зовёт ads.resume. Так самовосстанавливается и уже запущенная кампания — по
переходу она осталась бы выключенной навсегда, потому что «принято» у нас
записано давно. Состояние Яндекс присылал в каждом ответе и раньше, опросчик
его просто выбрасывал.
Проверено вырезанием: без починки новый тест краснеет. Реклама 349 из 349.
При 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>
Повод — письмо тревоги 31.07.2026 05:30 МСК: сверка №1417 отрапортовала «потеряно 7».
Разбор на бою показал, что вебхук по тем же семи пришёл через 2,5 минуты (05:32:32 ×2 +
05:33:03 ×5) и получил «уже есть». Потери не было: поставщик кладёт строку в журнал
отданного РАНЬШЕ, чем шлёт вебхук, а наша сверка (каждые 30 мин) попадала в эту щель.
Цена ошибки не только ложная тревога: добранная карточка беднее живой (в журнале нет
tag/time/phones) — у 4 из 7 не определился регион, у сделок пустые регион и город.
1. Отсрочка добора (CsvReconcileJob::GRACE_MINUTES = 15). Недостача, увиденная впервые,
уходит в карантин (Redis, карта vid => время первого обнаружения) и добирается только
следующим прогоном, если провисела дольше отсрочки. Реальная потеря доезжает максимум
через полчаса. Потеря карантина безопасна: в худшем случае добор на прогон позже.
drift и «потеряно» в письме считаются ТОЛЬКО по просроченному; «в пути» — отдельно
(новая колонка supplier_csv_reconcile_log.pending_count).
2. Журнал вебхука поставщика (новая таблица supplier_webhook_log). Прежний
logSupplierWebhook писал в webhook_log, снесённую 24.05 вместе с legacy-каналом, и
молча выходил по Schema::hasTable — журнала не было ВООБЩЕ. Из-за этого 70 отказов 404
за 10 дней никто не видел; нашлись случайно в логе nginx. Пишем статус, адрес
отправителя, запрошенный хост и отпечаток присланного ключа (первые 8 символов md5 —
отвечает «наш ключ или чужой», секретом не является). Отказы дополнительно уровнем
warning: на бою LOG_LEVEL=warning, info в журнал не попадает.
3. Текст письма-тревоги переписан: «потеря» = только то, что не дошло даже за отсрочку;
«в пути» показывается отдельной строкой и потерей не считается.
4. В catch сверки — Log::error ПЕРВЫМ действием, до обращений к БД: при испорченной
транзакции следующий запрос бросал своё исключение и настоящая причина терялась.
Проверено: тесты поставщика и вебхука 230/230 (в т.ч. 5 новых на карантин, поздний
вебхук, отчёт «в пути» и смоук шаблона письма), Larastan 0, Pint чисто. Прогон всей базы
3566/3575; 5 падений — чужие и до этих правок (замерено откатом файлов): биллинг на стыке
месяцев и маршруты телеграм-ветки.
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>
Номера телефонов в задание не кладём — только текст, ссылка и смета.
После выката на бой перезапустить db/03_service_bypass_policies.sql.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Живой запуск кампании на бою 30.07.2026 вскрыл две поломки, которых вчерашняя
починка не видела. Обе — из класса молчаливых: портал рапортует успех, а в жизни
ничего не происходит.
ПЕРВАЯ. Минимум Директа НЕ постоянный.
Вчера мы приняли живой отказ «Budget for this period cannot be less than 600 rub.»
за постоянный порог и зашили 600 ₽ в настройку. Сегодня та же кампания на неделю
получила отказ «cannot be less than 2400 rub.» — проверка её пропустила, и клиент
снова увидел английский текст.
Две точки дали правило: минимум = 300 ₽ за каждый календарный день периода,
считая оба края.
период 30.07-31.07, 2 дня → 600 ₽ = 2 × 300
период 30.07-06.08, 8 дней → 2400 ₽ = 8 × 300
Теперь порог считается от периода показа, а ставка за день вынесена в настройку
YANDEX_DIRECT_MIN_SPEND_RUB_PER_DAY. Период вычисляется ДО денег — иначе считать
минимум не от чего.
ВТОРАЯ. Создать объявления — не значит запустить рекламу.
Запуск проходил успешно, портал ставил статус «на модерации», а в кабинете лежали
кампания, группа и 15 объявлений в состоянии DRAFT и OFF. Яндекс кладёт всё
созданное черновиком и проверку сам не начинает. Реклама не показалась бы никогда,
и узнать об этом можно было только глазами в кабинете: наш журнал говорил, что всё
хорошо.
Добавлены два вызова, которых не было вовсе:
ads.moderate — отдать созданные объявления на проверку
campaigns.resume — включить показ
Порядок обязателен. Пока кампания черновик, включить её нельзя — Директ отвечает
«кампания является черновиком и не может быть остановлена». Сначала модерация,
она переводит кампанию в MODERATION, и только потом включение.
Отбор объявлений строго по Ids. На CampaignIds Директ отвечает «отсутствует
обязательный параметр Ids» — именно так отправка молча не сработала бы.
На модерацию уходят объявления, созданные ИМЕННО этим заходом: повторная отправка
уже проверяемого — отказ по позиции, он порвал бы возобновляемый запуск. Включение
показа безобидно при любом повторе: на включённой кампании Яндекс отвечает
предупреждением, а не отказом.
Заодно клиент Директа перестал молчать про отказы внутри ответа: раньше он смотрел
только AddResults, UpdateResults и DeleteResults, теперь ещё ModerateResults и
ResumeResults. Отказ по позиции в этих двух проходил бы насквозь незамеченным.
ПРОВЕРЕНО ЖИВЬЁМ. Кампания № 6 на бою: в кабинете 713175197, группа 5778555449,
15 объявлений, аудитория подключена, статус MODERATION, показ включён. На счету
Яндекса 8775 ₽. У клиента заморожено 3333.36 ₽ за 27778 показов до 06.08.
Тесты: 346 зелёных по рекламе, 43 по клиенту Директа, статанализ ноль замечаний.
Четыре новых теста — минимум по длине периода на боевом случае, отправка на
модерацию, порядок модерация-до-включения, отсутствие повторной отправки.
Два старых теста закрепляли частный случай в два дня — им проставлен явный срок
показа, иначе они молча описывали бы неправду.
Co-Authored-By: Claude Opus 5 <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>
Настоящая причина, по которой портал не мог завести кампанию. Прежний диагноз «сегменту
нужно не меньше 1000 опознанных людей» оказался ВЫДУМКОЙ: он был построен на статьях,
а не на замере, и увёл в ожидание, которое ничего не решало.
Что происходит. Сегмент Аудиторий указывается в условии ретаргетинга не своим номером,
а номером с приставкой 20: сегмент 58034825 уходит как ExternalId 2058034825. Без приставки
Директ ищет цель Метрики с таким номером и честно отвечает 8800 «Object not found» — тем же
отказом, что и на несуществующий сегмент. Поэтому поломка выглядела как «аудитория не готова».
Замер на боевом кабинете 30.07 на одном и том же ГОТОВОМ сегменте:
ExternalId 58034825 -> отказ 8800 «Object not found»
ExternalId 2058034825 -> условие создано, № 41678818
После починки то же самое кодом портала: условие № 41678854 создано и убрано за собой.
Приставку показал сам Яндекс: у уже существующего в кабинете условия на этот сегмент
retargetinglists.get возвращает ровно "ExternalId": 2058034825. Документация про приставки
говорит глухо и только «для интересов» — живой ответ кабинета оказался надёжнее документации.
Попутно снят ложный «дефект продукта». Порога в 1000 человек НЕ существует: сегмент годится
для Директа при 134 опознанных людях из 151 номера. Минимум портала менять не нужно.
Ещё один замер, важный на будущее: статус is_processed у сегмента — это НЕ «готов»,
у готового статус processed, заполнены matched_quantity и cookies_matched_quantity,
и стоит can_create_dependent. Судить о готовности только по can_create_dependent.
Тестов на приставку было ДВА, и оба закрепляли поломку: проверяли, что уходит номер БЕЗ
приставки. Оба были зелёные всё время. Тест, написанный по нашему представлению о правде,
а не по ответу живой системы, охраняет баг и делает его невидимым. Теперь оба проверяют
приставку и падают без неё; реклама портала 43 из 43.
Вместе с починкой ложатся три промта смен: 29.07 сведение телеграма и прокси, 29.07 робот
креативов в работу и 30.07 состояние после живой приёмки. В последнем ложный диагноз про
порог в 1000 человек помечен как неверный прямо в шапке — чтобы следующая смена не пошла
по нему второй раз.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ни одну нельзя было увидеть без настоящего прогона: часть закрыта песочницей Яндекса,
часть — швами между кусками, которые по отдельности покрыты тестами.
1. Слепок креативов уходил с SelectionCriteria пустым МАССИВОМ. Живой Яндекс отвечает
отказом 8000 «SelectionCriteria cannot contain an array», портал не может снять слепок,
задание роботу не выдаётся, клиент видит вечное «готовим картинки». В JSON нужен пустой
ОБЪЕКТ. Песочница до этой проверки не доходила — отвергала запрос раньше, на входе.
2. Отказы Яндекса ПО ПОЗИЦИИ внутри ответа глотались молча. Запрос успешен, а нужного Id
в позиции нет — вместо него Errors с человеческим объяснением. Портал падал ошибкой PHP
«Undefined array key Id», ни клиенту, ни в журнал не попадало ни слова из ответа Яндекса.
Теперь слова Яндекса выходят наружу — именно это и позволило найти пункт 3.
3. В условии ретаргетинга не передавался MembershipLifeSpan — срок хранения человека
в сегменте. Живой Яндекс отказывает «Required field: Not specified time for goal or
segment», и следом «Object not found»: довод негоден целиком, запуск встаёт. Берём
audience_days кампании, границы Яндекса 1..540.
4. Робот не отдавал набор Яндексу: заливал файлы, нажимал «Создать» и уходил. Оказалось,
«набор» в кабинете и «креатив» в creatives.get — разные вещи: пока набор не отмечен
галочкой и не нажато «Добавить выбранные», Яндекс креативы не регистрирует. Замер:
14 залитых картинок были невидимы для creatives.get, после добавления в ЧЕРНОВИК формы
счётчик прыгнул 18 -> 32 в ту же секунду. Объявление при этом не сохраняется,
«Сохранить изменения» робот по-прежнему не трогает никогда.
5. Гонка при заливке. Три живых прогона подряд из 15 картинок доносили 14, и каждый раз
пропадала ДРУГАЯ: 480x320, потом 300x600, потом 336x280. Замер объяснил: кнопка
«Создать» разблокируется на 4-й секунде, когда принято 11 файлов из 15, остальные
дозагружаются к 7-й. Робот жал сразу по разблокировке. Ждём теперь по строкам принятых
файлов и по исчезновению слова «Загружается».
Первая попытка этой починки НЕ РАБОТАЛА и выглядела рабочей: сторож считал размеры
в окне, не заметив постоянного фильтра из тех же 15 размеров вверху. Поймано по тому,
что прогон занял ровно столько же секунд, сколько до починки. Отсюда тире в признаке
приёма — фильтр его не содержит.
Каждая починка закрыта тестом, который сначала падал на своей поломке. Прежние тесты
проверяли разбор ОТВЕТОВ и путь ДО «Создать» — форму запросов и то, что после, не смотрел
никто. Робот 84 из 84, реклама портала 42 из 42.
Проверено живьём на боевом: задание роботу отработало со статусом done, опознано
15 креативов из 15.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прод-баг: 5 из 9 работающих проектов на бою показывали жёлтое «Готовим
к запуску», хотя заказ у поставщика реально стоял и лиды шли. У трёх
клиентов; самый старый врал 13 дней.
Причина. Связь проекта с заказом хранится в двух местах: три legacy-колонки
supplier_b{1,2,3}_project_id и pivot project_supplier_links. Статус читался
ТОЛЬКО из колонок. При этом ночной SyncSupplierProjectsJob — единственный,
кто в режиме batch реально заводит заказ, — пишет ТОЛЬКО в pivot и колонок
не касается вообще. Колонки заполняет лишь SyncSupplierProjectJob и только
если заказ УЖЕ существует в момент запуска; при создании проекта заказа ещё
нет, он появится в 18:00. Итог: колонки остаются пустыми навсегда, пока
клиент сам не дёрнет проект — пауза, снятие с паузы, «Синхронизировать»
или правка настроек.
Правка. resolvedSupplierProjects() возвращает объединение pivot и трёх
legacy-колонок без дублей. Legacy читаем дальше: handleBatch пишет колонку
без pivot-строки, такие проекты терять нельзя. getSupplierLinks() берёт
площадку из самой строки заказа. Списку и карточке добавлен eager-load
supplierProjects — иначе N+1 на каждую карточку.
Данные править не нужно: связки в pivot уже лежат, пятёрка чинится сама
в момент выката.
Тесты: 9 новых, 5 из них падали до правки. Полный прогон 3569 тестов:
3563 зелёных, 2 падения — чужие, в ClientTg\InputLengthFixTest, красные
и без этой правки.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Слияние feat/client-telegram-ads с main 8bdd58e8. Десять швов разобраны вручную:
денежный файл AdWalletService взят из main целиком — проверено поимённо, что все три
починки на месте: свой контекст клиента, оживление брони, таяние заморозки. В расписании
объединены оба набора заданий: телеграмные два и рекламные четыре. В боковом меню и в
мобильном «Ещё» сохранён пункт «Рекламный кошелёк», подписи поправлены — на реальные
экраны ведут ОБА канала. Словарь, пример настроек и журнал схемы объединены.
Сверх самого слияния:
- Журнал схемы: телеграмные записи v8.86-v8.95 перенумерованы в v9.18-v9.27, блок
переставлен наверх, пометки «номер предварительный» сняты и заменены одной врезкой
о перенумерации. Задвоенных номеров не осталось. Врезка шапки теперь называет и
телеграмные таблицы: их DDL, как и рекламный, живёт только в дельта-миграциях.
- Новый сторож денег tests/Feature/ClientTg/TgMoneyUnderRealRoleTest.php: списание и
возврат под боевой ролью crm_app_user. С контекстом клиента деньги двигаются, без
контекста возврат падает громко. Обычные тесты ходят суперюзером и этот класс дыр
увидеть не могут.
- Помощник rejectedCampaign переименован в tgRejectedCampaign: одноимённый помощник
есть у рекламного модуля, помощники Pest глобальные, полный прогон падал фаталом.
Каждая ветка по отдельности этого увидеть не могла.
- Два теста уведомлений считали ВСЮ таблицу целиком вместо строк своего пользователя:
в одиночку зелёные, в полном прогоне красные. Счёт сужен до конкретного пользователя.
- Убраны две проверки отменённой сущности «своё имя отправителя» — сама сущность
дропнута в v9.27 как СМС-фантазия, её адрес отдаёт 405.
Прогоны: телеграм 193/193, реклама 336/336 при 1029 проверках, вместе 532/532,
экраны 1704/1708, сборка фронта чисто, полный Unit+Feature 3923/3960. Шестнадцать
падений полного прогона совпадают построчно с прогоном ветки без телеграма — слияние
не добавило ни одного. Статанализ в свежем каталоге запустить не удалось: он требует
сгенерированного файла-подсказки, которого нет в репозитории, и без него молча падает
на обеих ветках.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Тот же класс, что нашли во втором проверяющем на СМС: чтение денег идёт вне куска
с контекстом клиента. Проверил свой модуль целиком по этому признаку.
Дыра одна, в самом больном месте. Джоб проверки модерации кампании перечисляет
кампании через обходное соединение, но возврат заморозки зовёт кошелёк на обычном
соединении и без контекста. Защита по клиентам на таблицах рекламы устроена строго:
без контекста сравнение с пустотой, и база отдаёт ноль строк.
Чем кончалось бы на бою: Яндекс отклонил весь набор объявлений - кампания помечена
отклонённой - возврат заморозки видит "кошелька нет", честно выходит - деньги клиента
остаются замороженными навсегда. Ни ошибки, ни строки в журнале, свободный остаток
занижен, новую кампанию запустить не на что.
Живой замер на опытной базе, откатанный сразу:
суперюзером, как ходят тесты - 1
боевой ролью без контекста, как читает джоб - 0
боевой ролью с контекстом - 1
Тесты ходят суперюзером, который защиту обходит, и увидеть это физически не могут.
Починка в самом кошельке, а не у вызывающего. Тенант приходит в кошелёк явным
доводом, значит и контекст - забота кошелька, а не каждого, кто его позовёт. Так
закрыт весь класс сразу, а не один случай.
Сторож гоняет деньги ПОД БОЕВОЙ РОЛЬЮ и без контекста. Написан до починки, падал
двумя разными способами: возврат молча ничего не делал, заморозка бросала "кошелька
нет". Опасен именно молчаливый.
Обход остальных фоновых путей: перечисление кампаний, сторож зависших заданий,
сборка аудитории, постановка задания разведки, запись в ленту - у всех защита на
месте. Список получателей уведомления читается из таблицы, у которой политика мягкая:
без контекста она открыта, ноль строк не даёт.
Реклама 395 из 395 при 1251 проверке, было 393. Админка и кошелёк 25 из 25.
На боевой не выкатывалось, никуда не отправлялось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено при разборе шва с веткой телеграм-рекламы: обе ветки правили один денежный
файл с разных сторон, и сравнение вскрыло поломку у нас.
Снятие заморозки не удаляет строку брони, а метит её снятой. На броне висит запрет
двух одинаковых записей по четвёрке тенант-канал-тип-источник. Повторная заморозка
той же кампании заводила строку заново и падала на дубле ключа.
По-человечески: клиент ставил кампанию на паузу и больше не мог её включить. Та же
дорога на новом пути отказ модерации - Исправить - отправить заново.
Почему 391 зелёный тест этого не видел. Есть два теста, и каждый честен по
отдельности: первый морозит и снимает, второй берёт кампанию, которую никогда
не морозили, и морозит. Последовательность снять и заморозить снова не проверял
никто - шов между двумя половинками остался голым.
Проверено прогоном, не рассуждением: база ответила дублирующееся значение ключа
нарушает ограничение уникальности ad_wallet_holds по ключу yandex campaign 1.
Починка взята у ветки телеграм-рекламы, которая наткнулась на то же самое: не
заводить бронь заново, а оживлять снятую. Оба сторожа написаны до починки и
проверены вырезанием - без неё падают, с ней проходят.
Реклама 393 из 393 при 1249 проверках, было 391. Админка и кошелёк 23 из 23.
На боевой не выкатывалось, никуда не отправлялось, рубильник Директа выключен.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 16 закрыта решением владельца. Дороги «робот везёт документ в кабинет Яндекса»
не существует — доказано двумя нарочными отказами, обычной тематикой и лицензируемой:
в окне отказа ноль полей для файла, документы Яндекс принимает только снаружи кабинета.
Приём документа оставлен, но портал больше не молчит: клиент приложил файл — в ленту сразу
ложится отметка «документ у нас, передать его Яндексу автоматически нельзя, при
необходимости отнесём сами и напишем здесь», а владельцу уходит письмо на адрес алертов.
Без письма обещание было бы пустым: файл просто лёг бы на диск. Сам файл письмом
не отправляем — это чужие бумаги. Обычный ответ без файла ни отметки, ни письма не даёт.
Экран «ждёт разбора» в админке: ручка была, экрана не было. Третья карточка на странице
«Реклама» — клиент, кампания, что робот делал человеческими словами, номер объявления
и на чём споткнулся. В подписи прямо сказано, чего там НЕ будет: обычных отказов,
их клиент разбирает сам.
Дальше — разбор собственной работы этого дня. Найдено четыре ошибки, все исправлены.
1. ТЯЖЁЛАЯ. Доклад разведки на бою уронил бы очередь заданий целиком. Робот пишет в ленту
под служебной ролью, а у неё на этой таблице было только чтение. Отказ по правам,
500 роботу, три повтора — и задание навсегда «в работе». Пока хоть одно задание
в работе, выдача отвечает «работы нет» ВСЕМ клиентам. Лечение — запись схемы v9.17:
право на запись плюс нумератор. В плане про это было написано прямым текстом,
я прошёл мимо. Тесты поймать не могли: ходят суперпользователем.
2. Признак «набор создан» я выдумал: взял метку, которая в нашей же разметке описана
как СКРЫТАЯ галочка. Проверка «видно ли её» не сработала бы никогда. Признак с экрана
убран совсем: успех определяет портал слепком креативов, а «окно не закрылось» —
это норма, так и есть живьём.
3. Сломал ленту для повторного отказа. Поменял защиту от дублей на «такой текст уже
когда-либо был» — и клиент, починивший рекламу и получивший тот же отказ, не увидел бы
ничего. Вернул сравнение с последним сообщением, а заглушку «причину выясняем» держит
теперь сам джоб: показываем один раз, пока сказать нечего.
4. Мой собственный тест оказался пустышкой: оставался зелёным при вырезанной защите.
В нём отклонялись ВСЕ объявления, а тогда кампания уходит в «отклонена» и обход её
больше не берёт. Сценарий существует только при частичном отказе — тест переписан
на два объявления и теперь вырезание защиты его роняет.
Портал 391/391, админские экраны 9/9, фронт на затронутых наборах 52/52, робот 74/74,
мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен, живьём разведка не гонялась.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 15. Программный интерфейс Яндекса причину отказа не отдаёт — на отклонённое
объявление приходит «Отклонено на модерации.» и всё. Причина висит только на экране
кабинета, и добыть её может лишь тот, у кого есть глаза.
Как теперь работает:
опрос модерации видит отказ и ставит роботу задание разведки по этому объявлению;
робот открывает список объявлений, находит ячейку своего объявления, кликает по статусу,
раскрывает строку причины, читает текст и снимает одно окно; доклад уезжает порталу формой
вместе со снимком; портал кладёт его в ленту от имени Яндекса слово в слово, клиенту
письмо и колокольчик. Робот не понял, что видит — задание сбойное, владельцу письмо,
в ленту клиенту ничего не сочиняем. В админке появилась ручка «ждёт разбора».
Четыре ловушки, пойманные по дороге и проверенные вырезанием:
1. Дедуп разведки нельзя вешать на кампанию. Отказ никуда не девается, а обход бежит
по расписанию: после закрытия первой разведки поставилась бы вторая, и робот ходил бы
в кабинет по кругу. Ключ — номер объявления, журнал схемы v9.16.
2. Рубильник держал не выдачу задания, а построение клиента Директа. Разведке слепок
креативов не нужен, значит при выключенном рубильнике она получила бы задание,
и робот пошёл бы в живой кабинет.
3. Дедуп ленты сравнивал только с последним сообщением Яндекса. После доклада робота
обход снова клал бы «причину выясняем» поверх настоящей причины.
4. Постановка разведки шла без tenant-контекста — на бою она не сработала бы ВООБЩЕ
и молча: поиск дубля давал бы ноль, запись падала бы на политике доступа, всё это
в предупреждение журнала при зелёных тестах. Поймал rls-reviewer. Лечение — своя
транзакция с выставлением клиента, рецепт ChargeCampaignSpendJob. Сторож поставлен
на сам механизм: обычным тестом это не ловится, они ходят суперпользователем.
Заодно: разведке больше не снимается слепок креативов — лишний поход в живой Яндекс
внутри открытой транзакции.
Портал 382/382, робот 75/75, мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>