feat реклама за показы: правда про документ, экран «ждёт разбора» и разбор своих ошибок

Задача 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>
This commit is contained in:
Дмитрий
2026-07-28 20:10:18 +03:00
parent 0ba7890bf9
commit 3fdcd88ad3
22 changed files with 979 additions and 195 deletions
@@ -452,3 +452,73 @@ it('рубильник выключен — ни обращения к Янде
expect(AdCreativeJob::where('campaign_id', $campaign->id)->count())->toBe(0)
->and($campaign->banners()->first()->moderation_status)->toBe(AdCampaignBanner::MOD_MODERATION);
});
/**
* 🪤 Ловушка, вскрытая разведкой. Порядок в жизни такой: обход кладёт клиенту «Яндекс
* причину не назвал, выясняем» робот приносит НАСТОЯЩУЮ причину следующий обход
* снова кладёт заглушку, ведь последним в ленте лежит уже доклад робота.
*
* Клиент, только что прочитавший настоящую причину, получил бы поверх неё «причину
* не назвали» и ещё одно письмо. Заглушку показываем ОДИН раз пока сказать нечего.
*
* 🔴 Кампания тут с ДВУМЯ объявлениями: одно приняли, одно отклонили. Иначе сценария
* не существует вовсе при отказе ВСЕХ объявлений кампания уходит в «отклонена»,
* и обход её больше не берёт. Первая версия этого теста была пустышкой ровно поэтому:
* вырезание защиты её не роняло.
*/
it('после доклада разведки заглушка «причину выясняем» в ленту не возвращается', function () {
configureYandexForModeration();
[$campaign, $banners] = makeModeratedCampaignWithBanners([9106, 9108]);
$otkloneny = $banners->first();
$otvet = ['result' => ['Ads' => [
['Id' => 9106, 'State' => 'OFF', 'Status' => 'REJECTED', 'StatusClarification' => '
Отклонено на модерации.'],
['Id' => 9108, 'State' => 'ON', 'Status' => 'ACCEPTED', 'StatusClarification' => 'Принято'],
]]];
Http::fake(['*' => Http::response($otvet)]);
// Первый обход — заглушка. Кампания остаётся работающей: одно объявление принято.
(new SyncCampaignModerationJob)->handle();
expect($campaign->refresh()->status)->toBe(AdCampaign::STATUS_RUNNING);
// Робот сходил и принёс настоящую причину.
app(CampaignMessageService::class)->postFromRobot(
$campaign, (int) $otkloneny->id, 'Нет предупреждения: финансовые услуги',
);
// Второй обход — заглушке в ленте больше не место.
(new SyncCampaignModerationJob)->handle();
$bodies = AdCampaignMessage::where('campaign_id', $campaign->id)
->where('banner_id', $otkloneny->id)
->orderBy('id')->pluck('body')->all();
expect($bodies)->toHaveCount(2)
->and($bodies[1])->toContain('финансовые услуги');
});
/**
* Обратная сторона: пока Яндекс молчит, а робот ещё не сходил, заглушка нужна
* иначе под ярлыком «Отклонено» у клиента будет пустота.
*/
it('пока разведка не сходила, заглушка клиенту всё-таки показывается', function () {
configureYandexForModeration();
[$campaign] = makeModeratedCampaignWithBanners([9107]);
Http::fake(['*' => Http::response(['result' => ['Ads' => [[
'Id' => 9107, 'State' => 'OFF', 'Status' => 'REJECTED',
'StatusClarification' => '
Отклонено на модерации.',
]]]])]);
(new SyncCampaignModerationJob)->handle();
$first = AdCampaignMessage::where('campaign_id', $campaign->id)->first();
expect($first)->not->toBeNull()
->and($first->body)->toContain('причину не назвал');
});