fix реклама за показы: предел веса картинки проверен по-настоящему, обход модерации не срывается целиком, робот не несёт токен на чужой адрес

Мелочи приёмочного листа v12 §8. Каждая правка с тестом; где защита уже стояла
в коде — тест проверен вырезанием этой защиты.

Предел веса картинки. Две прежние проверки были пустышками: сравнивали константу
саму с собой и с тем же числом в ответе сервера. Вырезание правила max: оставляло
обе зелёными. Настоящий тест грузит перевес и ждёт отказа — это четвёртая найденная
пустышка за ветку.

Обход модерации. Объявление без статуса и причина отказа длиннее колонки роняли
запись в базу ВНЕ защиты, и обход обрывался на середине: остальные клиенты не узнавали,
приняли их рекламу или отклонили, а деньги за отклонённый набор не возвращались.
Запись ответа теперь под той же защитой, что и сеть; пустой статус не пишем вовсе,
причину храним обрезанной.

Робот. Адрес файла приходил в ответе сервера, а шли по нему со своим токеном без
всякой сверки. Теперь адрес обязан вести на портал. Папка снимков экрана росла
бесконечно, а на снимках видны логин и остаток счёта — старше двух недель убираются.

Ещё: порядок посредников служебного канала — токен раньше служебного соединения;
BannerGenerator больше не отдаёт молча файл тяжелее предела и берёт предел из общей
константы; нулевой номер креатива ловится намеренно, а не случайно нестрогим сравнением.

Портал 300/300, робот 60/60. Мест снятия заморозки денег по-прежнему четыре.

Не тронуто намеренно: цена за 1000 показов и бюджет приходят от клиента — но это
видимое поле мастера и принятое продуктовое решение, а не недосмотр. Решает владелец.
This commit is contained in:
Дмитрий
2026-07-28 07:21:02 +03:00
parent 0f82a2d9ff
commit af12b1ceb4
15 changed files with 480 additions and 23 deletions
@@ -121,6 +121,59 @@ it('waits while at least one banner ad is still under moderation', function () {
expect($campaign->fresh()->status)->toBe(AdCampaign::STATUS_PENDING_MODERATION);
});
/**
* Обход модерации идёт по ВСЕМ кампаниям всех клиентов подряд. Значит любая беда на одной
* кампании обязана остаться внутри неё: сорвётся обход остальные клиенты не узнают, что
* их реклама принята или отклонена, а деньги за отклонённый набор не вернутся.
*
* Здесь беда приходит не из сети (это уже прикрыто), а из САМОГО ответа Яндекса: объявление
* пришло без статуса. Запись пустого статуса упирается в запрет базы, и обход обрывается
* на середине.
*/
it('объявление без статуса не срывает обход остальных кампаний', function () {
configureYandexForModeration();
Http::fake(function ($request) {
$ids = $request->data()['params']['SelectionCriteria']['Ids'] ?? [];
return Http::response(['result' => ['Ads' => in_array(6001, $ids, true)
? [['Id' => 6001, 'State' => 'ON']] // статуса нет вовсе
: [['Id' => 6002, 'State' => 'ON', 'StatusClarification' => null, 'Status' => 'ACCEPTED']],
]]);
});
[$broken, $brokenBanners] = makeModeratedCampaignWithBanners([6001], 601);
[$healthy] = makeModeratedCampaignWithBanners([6002], 602);
(new SyncCampaignModerationJob)->handle();
// Кампания без статуса осталась ждать — и не утащила за собой соседнюю.
expect($broken->fresh()->status)->toBe(AdCampaign::STATUS_PENDING_MODERATION)
->and(AdCampaignBanner::find($brokenBanners[0]->id)->moderation_status)->toBe(AdCampaignBanner::MOD_MODERATION)
->and($healthy->fresh()->status)->toBe(AdCampaign::STATUS_RUNNING);
});
/**
* Причина отказа у Яндекса бывает длинной там перечисляют все претензии модератора списком.
* В нашей колонке 255 знаков. Длинная причина упирается в базу и обрывает тот же обход.
* Причину показываем клиенту, поэтому храним сколько влезает, а не теряем целиком.
*/
it('слишком длинная причина отказа не срывает обход', function () {
configureYandexForModeration();
$longReason = str_repeat('Текст на баннере не читается. ', 40); // 1200 знаков
Http::fake(['*/json/v5/ads' => Http::response(['result' => ['Ads' => [
['Id' => 6011, 'State' => 'OFF', 'StatusClarification' => $longReason, 'Status' => 'REJECTED'],
]]])]);
[$campaign, $banners] = makeModeratedCampaignWithBanners([6011], 611);
(new SyncCampaignModerationJob)->handle();
$reason = AdCampaignBanner::find($banners[0]->id)->moderation_reason;
expect($campaign->fresh()->status)->toBe(AdCampaign::STATUS_REJECTED)
->and(mb_strlen((string) $reason))->toBe(255)
->and($reason)->toStartWith('Текст на баннере не читается.');
});
// ВЫХОД 2 остаётся ровно один: разморозка зовётся только когда отклонены ВСЕ
// объявления набора. Пока живо хоть одно — деньги остаются замороженными.
it('не снимает заморозку, пока принято хотя бы одно объявление набора', function () {