Приёмочный лист v12 пройден целиком: восемь критичных дыр, хвосты портала и денег,
хвосты робота и мелочи §8. Осталось только то, чего нельзя сделать без владельца
и без живого кабинета — задачи 16 и 17.
Владелец подтвердил 28.07.2026: цену за 1000 показов назначает клиент. Пункт листа
про client_cpm_rub и budget_rub закрыт как решение, а не как дыра — поле в мастере
остаётся, ручки продолжают принимать цену. Записано и в журнале починки, и в промте,
чтобы будущий разбор не переоткрыл вопрос заново.
Портал 300/300, робот 60/60, мест снятия заморозки денег четыре.
Мелочи приёмочного листа v12 §8. Каждая правка с тестом; где защита уже стояла
в коде — тест проверен вырезанием этой защиты.
Предел веса картинки. Две прежние проверки были пустышками: сравнивали константу
саму с собой и с тем же числом в ответе сервера. Вырезание правила max: оставляло
обе зелёными. Настоящий тест грузит перевес и ждёт отказа — это четвёртая найденная
пустышка за ветку.
Обход модерации. Объявление без статуса и причина отказа длиннее колонки роняли
запись в базу ВНЕ защиты, и обход обрывался на середине: остальные клиенты не узнавали,
приняли их рекламу или отклонили, а деньги за отклонённый набор не возвращались.
Запись ответа теперь под той же защитой, что и сеть; пустой статус не пишем вовсе,
причину храним обрезанной.
Робот. Адрес файла приходил в ответе сервера, а шли по нему со своим токеном без
всякой сверки. Теперь адрес обязан вести на портал. Папка снимков экрана росла
бесконечно, а на снимках видны логин и остаток счёта — старше двух недель убираются.
Ещё: порядок посредников служебного канала — токен раньше служебного соединения;
BannerGenerator больше не отдаёт молча файл тяжелее предела и берёт предел из общей
константы; нулевой номер креатива ловится намеренно, а не случайно нестрогим сравнением.
Портал 300/300, робот 60/60. Мест снятия заморозки денег по-прежнему четыре.
Не тронуто намеренно: цена за 1000 показов и бюджет приходят от клиента — но это
видимое поле мастера и принятое продуктовое решение, а не недосмотр. Решает владелец.
Все восемь критичных дыр и все хвосты закрыты, кроме Д4 — это вопрос владельца.
В промте: где мы сейчас, что нельзя переделывать и почему, оставшиеся мелочи из
листа v12 параграф 8, известные остатки, найденные по ходу, и накопленные грабли.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Хвосты денег Д1-Д6 и робота Р-х1-Р-х6 из приёмочного листа v12.
Д1 цена, по которой заморожены деньги, записывается на кампанию. Пока поле было
пустым, списание читало глобальную цену — админ менял её, и клиент платил больше
обещанного при запуске.
Д2 суточное списание берёт кампанию под замком строки. Ключ идемпотентности зависит
от числа показов, поэтому два одновременных прогона получали разные ключи и списали
бы клиента дважды.
Д3 пауза, не дошедшая до Директа, больше не считается паузой: отказ 409, заморозка
остаётся. Раньше реклама крутилась дальше, портал показывал паузу, а деньги были уже
свободны. У возобновления поведение намеренно прежнее — иначе понадобилось бы пятое
место разморозки, а их ровно четыре. Там же убрана мина строгого сравнения рубильника.
Д4 не трогали — это вопрос владельца.
Д5 рубильник Директа держит и служебный канал робота: выдача задания и приём отчёта
ходили в живой кабинет мимо него.
Д6 проверка рубильника приведена к общему виду: YANDEX_DIRECT_ENABLED=0 давало строку,
которую строгое сравнение читало как включено.
Р-х1 настройки читаются из .env робота, а не каталога запуска.
Р-х2 файл-замок robot.lock: проход и поддержание входа больше не дерутся за профиль
браузера. Занят — уходим молча, задание остаётся в очереди. Брошенный замок
перехватывается через полчаса.
Р-х3 письмо-алярм честно говорит, залиты ли уже креативы в кабинет. Побочно вскрылось,
что тексты писем не проверялись ни одним тестом — транспорт вынесен в src/smtp.js.
Р-х4 тест-пустышка про рабочую папку заменён настоящим: запуск из чужого каталога без
явной папки. Проверено вырезанием.
Р-х6 пустое значение в окружении читается как значение по умолчанию, мусор даёт внятную
ошибку вместо тихого NaN.
Портал 293/293, робот 57/57. Денежных выходов снятия заморозки по-прежнему четыре.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Хвосты портала П1-П8 из приёмочного листа v12.
П1 обрыв постановки задания больше не даёт клиенту голый 500 — 503 с человеческим
текстом и записью в журнал. Таймаут у HTTP-клиента Laravel уже был, эта половина
находки не подтвердилась.
П2 и П6 роботу отдаются только баннеры без номера креатива — тот же список
сопоставляется при отчёте. Раньше стороны расходились и опознание падало на
безупречной работе робота, плодя дубли в кабинете. Плюс постраничный обход описи
креативов: слепок обрывался на первой странице.
П3 слепок «до» снимается при выдаче задания, а не при постановке, и в той же
транзакции, что и перевод в работу. Два задания в очереди получали одинаковый
слепок, второе падало всегда. Постановка перестала зависеть от живости Яндекса.
П4 перед созданием объявлений сверяется настоящий размер каждого креатива одним
запросом. Не сошлось или креатива нет — запуск не идёт.
П5 уникальный индекс uq_ad_campaign_banner_slot, запись v9.09 в CHANGELOG схемы,
rls-reviewer GO.
П7 замок на правку расширен на сегмент Аудиторий — он создаётся раньше кампании.
Смежная находка: перезаливка картинки теперь обнуляет номер креатива.
П8 и Р-х5 файл отдаётся под настоящим расширением и типом содержимого, имя от
номера баннера; робот берёт расширение из ответа портала.
Портал 287/287, робот 43/43. Денежных выходов снятия заморозки по-прежнему четыре.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Р6 — задание, брошенное «в работе», держало очередь для всех клиентов навсегда.
Причина сбоя обрезается до 900 знаков, иначе портал отвергал отчёт о самом частом
виде поломки. Провал отчёта больше не глотается молча — едет в письмо человеку.
Приём отчёта ловит любую ошибку и закрывает задание сбойным вместо 500 роботу.
Новый сторож creative-jobs:reap каждые 10 минут разбирает пробку.
Р7 — обрыв на докладе «готово» объявлял провалом уже сделанную работу. Доклад
вынесен из-под обработчика сбоя, повторяется трижды, при неудаче только письмо.
Осечка ухода со страницы после «Создать» больше не считается провалом загрузки.
Р8 — пустой список размеров считался успехом, и робот заливал одни и те же файлы
по кругу, оставляя мусор в живом кабинете. Теперь громкий отказ до похода в Яндекс.
Портал 274/274, робот 41/41. Все защиты проверены вырезанием.
Выходов снятия заморозки денег по-прежнему четыре.
Где мы: пять дыр из восьми починены и проверены вырезанием, портал 266/266,
робот 35/35. Промт v12 остаётся приёмочным листом — v13 только про текущее
состояние, следующую работу и грабли, пойманные по дороге.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Р3 хвост. Адрес файла баннера стал /api/creative-robot/jobs/{jobId}/banners/{bannerId}/file.
Раньше портал подставлял «какое-нибудь задание в работе» — защита выдачи чужих картинок
держалась на внешнем условии, а не на самом запросе. Робота править не пришлось: он берёт
адрес из ответа портала как есть. Запись v9.08 в журнал схемы про частичный уникальный
индекс; rls-reviewer по миграции — GO.
Р4. Замок на наборе баннеров после заведения кампании в Яндексе: перезаливка, включение
и удаление отдают 409 по тому же признаку yandex_campaign_id, что и замок на параметрах.
Без него в портале была новая картинка, а в Яндексе крутилась старая, а удаление строки
с номером объявления заставляло возобновление завести второе объявление того же размера —
старое продолжало крутиться за деньги клиента. Баннер с номером объявления не удаляется
никогда.
Р5. Захват кампании под запуск: новый промежуточный статус launching, перевод под замком
строки в транзакции. Два одновременных нажатия «запустить» проходили проверку статуса оба
и заводили две CPM-кампании при одной заморозке денег. На любой ошибке прежний статус
возвращается, номера созданных в Яндексе сущностей уцелевают — возобновляемый запуск
не тронут. Брошенный захват старше пятнадцати минут перехватывается.
Тесты: портал 266/266, робот 35/35. Все новые защиты проверены вырезанием.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Р1. Кампания, не добравшая смету показов, оставалась running навсегда, а заморозка
денег клиента - ACTIVE навсегда: единственным переходом в completed было
delivered >= paid_impressions, а задачи, закрывающей кампанию по истечении срока
показа, не существовало вовсе. Для медийки по списку телефонов недокрут сметы -
типовой исход, а не редкий случай.
Новая колонка ad_campaigns.shows_until хранит последний день показа - ровно тот,
что уходит в Директ параметром EndDate. Пишется вместе с yandex_campaign_id, то
есть в момент, когда дату начинает держать Яндекс; возобновляемый запуск
переиспользует уже записанную дату, чтобы портал и Директ считали срок одинаково.
У выхода 1 в CampaignImpressionCharger появилось второе условие - новых мест
вызова AdWalletService::release не добавилось, их по-прежнему ровно четыре.
Запись v9.07 в журнале схемы, rls-reviewer GO.
Р2. Отчёт робота принимался по любому заданию в любом состоянии: номер брался из
адреса как есть. Готово по чужому ещё не выданному заданию разложило бы номера
креативов чужой кампании по её баннерам - картинка одного клиента уехала бы в
объявление другого; сбой по уже закрытому заданию переписал бы правильный
результат на failed. Теперь done принимает отчёт только по заданию в статусе
taken - 409 в остальных случаях, та же проверка продублирована в сервисе.
Р3, первая половина. Проверка «в работе никого» в takeNext не блокировала строку:
две одновременные выдачи обе её проходили и уносили разные задания. Слепки
creatives.get перемешивались, а размеры блоков у всех клиентов одинаковые, поэтому
итог - тихая привязка чужого номера креатива. Гарантию даёт частичный уникальный
индекс uq_creative_job_single_taken, плюс advisory-замок первой строкой транзакции,
чтобы штатный путь спокойно отвечал роботу «работы нет».
Осталось по Р3: привязать выдачу файла к номеру задания, rls-reviewer по индексу,
запись v9.08 в журнал схемы. Ход работы - в файле PROGRESS рядом с промтом v12.
Тесты, прогнаны в одиночку: портал 256/256, робот 35/35. Все девять новых тестов
были красными до правок, каждая защита проверена вырезанием.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разбор всей ветки четырьмя проверяющими: робот, канал портала и RLS, деньги,
ядро на набор креативов. Восемь критических находок, все перепроверены по коду:
заморозка денег навсегда при недокруте сметы, отчёт робота принимается по любому
заданию, гонка на выдаче заданий, незапертые ручки баннеров, повторный запуск
кампании, очередь встающая колом, ложный отчёт о сбое, бесконечная заливка мусора.
Плюс важные хвосты и мелочи с файлами и строками.
Ничего из этого на бою нет: рубильник Директа выключен, ветка не выкачена.
Любой сбой даёт отчёт порталу и письмо со снимком экрана, кампания остаётся
черновиком. Отчёт порталу идёт ПЕРВЫМ: упавшая почта не должна подвешивать
задание в работе. Картинки клиента стираются при любом исходе.
Поправлены два расхождения плана с живым кодом: в кабинет уходят пути к файлам,
а не описания баннеров, и папки считаются от корня робота, а не от каталога
запуска - робота запускает расписание.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Заглушка плана заменена настоящим кодом: разметка снята в задаче 11.
Прихожая — существующие кампания и группа из настроек: своей кампании у робота
в этот момент ещё нет, портал ставит задание до её создания в Яндексе.
Окно, не закрывшееся после Создать, провалом НЕ считается — оно может остаться
открытым и после удачной загрузки. Робот закрывает его крестиком и отдаёт слова
кабинета наружу, а приговор выносит портал по слепку креативов.
Кнопку Сохранить изменения робот не нажимает никогда — на это есть тест-сторож.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Живая разметка кабинета снята и закоммичена, поэтому в промте появился раздел
с готовыми селекторами и главным выводом — системное окно выбора файла роботу
не нужно, файлы кладутся прямо в поле формы.
Добавлено: признаки живого входа и слетевшего входа, единственная опасная кнопка
на пути робота, чего в разметке нет и что добирается в задаче 14, а также
новое открытое решение владельца про имена креативов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Снято живьём вместе с владельцем в кабинете sasha261185, кампания 713051718.
Ничего не сохранялось и не создавалось: до и после — те же 15 объявлений,
кнопки «Сохранить изменения» и «Создать» не нажимались.
Ключевое для задачи 14: системное окно выбора файла роботу не нужно —
в форме есть input type=file с data-testid CreativeActionsMenu.FileInput,
принимает png/jpeg/gif и сразу несколько файлов. Весь путь до окна загрузки
проверен программным прогоном, окно открылось.
Подтверждено живьём: конструктор креативов закрыт, Яндекс пишет это сам;
у всех 15 креативов имя «Новый креатив», потому что поле имени одно на всю
загрузку — опознание по разнице слепков и размеру остаётся единственным
надёжным способом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Портал ставит роботу задание, когда у баннеров ещё нет номеров креативов: вместо
ошибки клиент видит «готовим картинки», кампания остаётся черновиком, деньги не
морозятся. Робот берёт задания строго по одному — иначе слепки креативов до и
после перемешаются, и опознать их будет нельзя.
Канал робота закрыт своим сервис-токеном, внесён в исключения проверки CSRF и
отдаёт файл только того задания, которое сейчас в работе. Постановка задания
стоит внутри проверки рубильника Директа — при выключенном рубильнике портал в
Яндекс не ходит.
Права на новую таблицу выданы роли crm_admin_user: канал идёт через посредник
admin-db, подменяющий подключение. Нумератор выдан crm_app_user — он единственный
вставляет строки. Журнал схемы — запись v9.06.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Запуск кампании стал возобновляемым: номер каждой созданной в Яндексе сущности
пишется на кампанию сразу, повторный вызов переиспользует созданное и заводит
объявления только для баннеров без номера. Номер группы записывается лишь после
успешной привязки аудитории — инвариант «есть номер группы, значит аудитория на
ней висит». Обрыв связи больше не оставляет кампанию-сироту в кабинете.
Двойной заморозки денег не было и раньше — AdWalletService::freeze идемпотентен
по активному холду; закрыто тестом. Денежный код не тронут, выходов снятия
заморозки по-прежнему четыре.
Два замка: launch отказывает из любого статуса кроме draft и queued — раньше
повтор откатывал статус и launched_at; update отказывает в правке параметров
показа, как только у кампании есть yandex_campaign_id — раньше клиент мог
поменять смету, цену и адрес сайта у работающей кампании, и портал молча
расходился с Яндексом. Название менять по-прежнему можно.
Права: GRANT SELECT, UPDATE на ad_campaign_banners роли crm_admin_user — без
него админ-экран увидел бы тихий ноль. GRANT USAGE, SELECT на нумераторы семи
рекламных таблиц роли crm_app_user — bigserial без USAGE даёт отказ на бою, а на
dev невидим из-за суперпользователя. Корневая причина в db/02_grants.sql —
ALTER DEFAULT PRIVILEGES без FOR ROLE crm_migrator — вынесена отдельным вопросом
к владельцу.
Миграция 2026_07_27_100000 получила гард на существование колонок под
прод-порядок выката migrate --pretend --force плюс ручной psql.
Журнал схемы v9.04 и v9.05, формулировка проверки в v9.04 уточнена честно.
229 из 229 тестов рекламы зелёные, squawk чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Конструктор креативов Яндекса закрыт 01.06.2026 — адаптивного креатива на все
размеры не существует. Медийная кампания состоит из объявления на каждый размер
блока со своим креативом, поэтому строка баннера стала единицей
размер плюс файл плюс креатив плюс объявление.
Что сделано:
- у баннера появились номер креатива, номер объявления и статус модерации
- кап веса баннера поднят со 150 КБ до предела Яндекса 512 КБ
- слепок картиночных креативов аккаунта через creatives.get
- опознание своих креативов разницей слепков до и после загрузки по размеру
- запуск заводит объявление на каждый включённый баннер
- модерация считается по каждому объявлению: кампания работает, если принято
хотя бы одно, а деньги возвращаются только когда отклонены все
Заморозка и возврат денег не тронуты, денежных выходов по-прежнему четыре.
После выката на прод ПЕРЕзапустить db/03_service_bypass_policies.sql, иначе
джоб модерации молча увидит ноль баннеров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
План — три фазы: ядро переходит на креатив у каждого баннера, канал заданий
между порталом и роботом, сам робот-грузчик на Playwright.
Попутно чинятся кап веса баннера 150 КБ до 512 КБ и правило модерации,
по которому один забракованный баннер валил всю оплаченную кампанию.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Живая проверка боевого кабинета опровергла допущение, на котором стояла Часть C и
часть уже написанного ядра. Конструктор креативов Яндекса закрыт 01.06.2026 —
Яндекс пишет об этом сам плашкой в редакторе объявления, показы его креативов
остановлены с 30.06. Адаптивного креатива из одной картинки не существует: медийная
кампания устроена как объявление на каждый размер блока со своим креативом, что
подтверждает наша живая кампания прогрева из 15 объявлений.
Отдельно проверен read-only запрос creatives.get с боевым токеном: метод отдаёт наши
баннеры, тип у них HTML5_CREATIVE, имя у всех одинаковое «Новый креатив» — опознавать
по имени нельзя, — и приходит PreviewUrl, годный для показа клиенту.
Отсюда дизайн: робот становится грузчиком готовых файлов, а не оформителем креатива.
Номера портал берёт сам через creatives.get слепком «до и после» с сопоставлением по
ширине и высоте. Ядро переходит с одного креатива на кампанию на креатив у каждого
баннера. Решения владельца: файлы готовит клиент, робот сам опрашивает портал, живёт
на боевом сервере, вход заводится через экран и удалённый рабочий стол на localhost.
Дописаны три вещи, не проговорённые в обсуждении: правило частичной модерации —
кампания работает, если принято хотя бы одно объявление, разморозка только когда
отклонены все; минимум один файл и не более одного на размер; раздел честно
неизвестного вместо выдуманных ответов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка живьём на восьми статусах рекламных кампаний: зелёный и жёлтый уже
были закрыты слоем правок от 14.05.2026 и дают 6.06 и 6.37 — прежнее
предположение, что они не проходят, оказалось неверным. Тот слой закрыл только
два цвета из четырёх: info давал 3.63 и error 4.31 при норме WCAG 2.1 AA 4.5.
Дописаны в тот же слой, той же глубины: info #2e5a6d даёт 5.87, error #952c2c
даёт 5.98. Подложка чипа берёт цвет из color-пропа и не меняется — темнеет
только текст, вид почти прежний. Задеты статусы «Готова к запуску»,
«Отклонено», «Остановлено (нет денег)» и все тональные чипы портала.
Заодно вторая копия карты статусов в CampaignReportDialog оставалась на обычном
grey (2.42) — приведена к grey-darken-2, как в списке кампаний.
Мерил на живой странице по .v-chip__content с учётом подложки, а не по токену
темы — прежняя ошибка была именно в этом.
Проверено: рекламные фронт-тесты 17/17; Pa11y по всем 24 адресам — 20 из 24 как
и до правки, регрессий нет, рекламные и все админские экраны дают 0 ошибок.
Боевого не касается, в main не влито.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Брендовые success и warning нечитаемы текстом: success даёт 4.25:1 на белой
карточке и 3.83:1 на ивори, warning — 2.25 и 2.03 при норме WCAG 2.1 AA 4.5:1.
Сами цвета не тронуты — остаются для заливок, иконок и рамок. Рядом добавлены
затемнённые текстовые двойники success-strong #256F46 и warning-strong #8A6410,
тон сохранён.
Все 15 мест text-success и 8 мест text-warning переведены на новые классы —
13 файлов: командный центр, биллинг и инциденты админки, система, тенанты,
интеграция с поставщиком, карточка тенанта, диалог баланса, рекламный
админ-экран, мастер рекламной кампании, массовые диалоги дней и регионов,
реквизиты в настройках.
Проверено: фронт 226 файлов / 1663 теста зелёные; Pa11y по всем 24 адресам —
рекламные и ВСЕ админские экраны дают 0 ошибок. Побочно вскрылось 12 старых
недочётов на клиентских экранах — записаны в находку, к этой правке отношения
не имеют.
Боевого не касается, в main не влито.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Pa11y: в конфиг добавлены три рекламных экрана — клиентский Яндекс,
рекламный кошелёк и админский расход/маржа.
- Чипы статусов «Черновик» и «На паузе» были на обычном grey — контраст
2.42:1 при норме WCAG 2.1 AA 4.5:1. Заменены на grey-darken-2, после
чего оба клиентских рекламных экрана дают 0 ошибок.
- CampaignLauncherTest коммитил кампании в статусе pending_moderation
с сегментом и сотней телефонов и не откатывал их. При полном прогоне
SyncCampaignAudienceJob перечисляет кампании всех тенантов через
pgsql_supplier и подхватывал эти хвосты — SyncCampaignAudienceJobTest
падал на «ни одного обращения к Яндексу». Добавлен DatabaseTransactions.
Замер: раньше после прогона папки оставалось 2 живых кампании, теперь 0.
Полный прогон: было 5 падений, стало 3 — оба рекламных ушли.
- Находка про контраст палитры портала вынесена отдельным документом:
зелёный 4.25 и жёлтый 2.25 на белом не проходят AA, но это брендбук
и уже на бою — решение за владельцем.
Реклама 156/156. Боевого не касается, в main не влито.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Риск «заморозка не снимается» в снимке помечен закрытым — коммит dd894a52.
Промт v5 самодостаточен: где что лежит, что сделано, что осталось по приоритету,
грабли окружения.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Заморозка ставилась один раз при запуске на всю смету показов и не снималась
нигде — AdWalletService::release не вызывался ни одной строкой приложения.
Главное следствие было блокирующим: charge уменьшал balance_rub, но не трогал
frozen_rub, поэтому одни и те же рубли считались дважды. Свободный остаток
balance − frozen уходил в минус, а AdWalletGate::isSolvent вызывается сразу
после списания в ChargeCampaignSpendJob — клиент объявлялся неплатёжеспособным
после первого же суточного списания, и AdStopAll глушил все его кампании.
Кампания умерла бы после первого дня показов даже при полном кошельке.
Что сделано:
- charge уменьшает активный холд на списанную сумму, холд закрывается при нуле;
- release стал идемпотентным — отсутствие кошелька или холда больше не ошибка;
- выход 1 completed — CampaignImpressionCharger возвращает остаток резерва;
- выход 2 rejected — SyncCampaignModerationJob возвращает резерв целиком;
- выход 3 stopped_no_funds — PauseCampaignsOnAdStop снимает резерв;
- выход 4 paused — контроллер снимает резерв, resume морозит остаток сметы
до обращения к Директу и отдаёт 409 с понятным текстом при нехватке денег.
Удаление кампании выходом не является — destroy разрешён только для черновика,
а черновик ещё не заморожен.
Решение по паузе согласовано с владельцем 27.07.2026: на паузе деньги свободны.
Тесты: рекламный модуль 156/156. Переписан сценарий одного существующего теста
ChargeCampaignSpendJobTest — нехватку денег теперь создаёт резерв ВТОРОЙ
кампании, так как прежняя постановка опиралась на двойной счёт и стала
недостижимой; проверяемое требование сохранено.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Состояние Части 4 автозапуск через API Директа: собрано и закоммичено в ветку,
не влито, не на проде, рубильник ВЫКЛ. Список коммитов, единая денежная модель
показы плюс 40%, чек-лист go-live Задача 9, открытые риски заморозка не снимается
при завершении release не зовётся, формат отчёта Директа, грабли окружения
изоляция теста и larastan в worktree. Дальше РОБОТ Часть C и ВЫБОР Часть A.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 10, фиксы найденного ревьюером в денежном ядре Части 4:
- CampaignLauncher при запуске ставит paid_impressions = смета показов. Это потолок
биллинга: счётчик показов ограничивает списание сметой и завершает кампанию по её
достижении. Раньше поле не ставилось никогда → списание шло без потолка до
SpendLimit ×guard и кампания не завершалась.
- CampaignImpressionCharger держит charged_client_rub монотонным high-water: при
просадке накопительного отчёта Директа база не опускается → нет двойного списания
при последующем росте.
- YandexDirectClient getCampaignImpressions шлёт уникальный ReportName на каждый
вызов, иначе Яндекс отдаёт кэш ALL_TIME-отчёта и списание замирает недобор.
- effectiveCpm без float, через bcmath.
Реклама-модуль 196/196 зелёный. Заморозка при завершении не снимается это отдельная
пред-существующая дыра модели кошелька release не зовётся нигде вынесено владельцу.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 8e. SaaS-оператор видит кампании в статусе queued по всем тенантам и
вписывает номер оформленного в конструкторе Яндекса адаптивного креатива
yandex_creative_id, после чего кампанию можно запускать. Два эндпоинта
AdminAdvertisingController campaignsAwaiting и setCampaignCreative под
saas-admin+admin-db, карточка в AdminAdvertisingView, api-функции и типы.
🔴 Запись строго через сырой DB::table update только колонки yandex_creative_id
Eloquent-билдер добавил бы updated_at, а колоночный GRANT у crm_admin_user
разрешает писать лишь эту колонку тесты идут под суперюзером и это не ловят.
RLS не трогаем ad_campaigns уже покрыт srv_bypass и грантами.
Бэкенд AdminAd 19/19, реклама-модуль 195/195, фронт админки 11/11.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 8d. Клиент вписывает в мастере адрес сайта landing_url, куда ведёт
баннер по клику. Поле на шаге «Баннеры», проверка перед переходом дальше
непустой и начинается с http, сохранение через patchCampaign, показ в
сводке шага «Проверка и отправка». Бэкенд store/update валидируют url
до 1024 символов. Так кампания не застрянет на запуске без адреса.
Реклама-модуль 192/192 зелёный на чистой базе, мастер 39/39. Правки
ортогональны деньгам.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 8c. После перевода запуска на медийную кампанию за показы удалены
неиспользуемые методы клиента YandexDirectClient: addCampaign TextCampaign,
addAdGroup, addAudienceTarget с ContextBid, addTextAd прод-вызовов нет.
Из модели AdCampaign убраны легаси клик-поля weekly_budget_rub и click_bid_rub
из fillable и casts колонки в БД не тронуты.
uploadAdImage и текстовые объявления оставлены живыми они ещё используются.
Тесты клиента и модели обновлены. Реклама-модуль 188/188 зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>