Files
portal/app/tests/Feature/Advertising/AdvertisingScheduleTest.php
T
Дмитрий 38fae090f8 fix(реклама): число показов узнаём каждый час, а не раз в сутки
Владелец сверил карточку с кабинетом Яндекса: в отчёте «Показано 0», а в
Директе за этот день один показ.

Причина наша. delivered_impressions — единственное число, из которого карточка
берёт «Показано», — обновляется только внутри ChargeCampaignSpendJob, а он ходил
раз в сутки в 04:20. Кампанию #11 запустили в 04:39, на двадцать минут позже
обхода, и до следующего утра клиент видел ноль при идущих показах.

Обход стал ежечасным. Чаще списывать безопасно по устройству счётчика: он
идемпотентен по ключу «yandex-imp:кампания:показы» и монотонен — уходит только
прирост, повторный прогон с тем же числом показов денег не трогает. Сторож
держит расписание 0 * * * *, принят красным на «20 4 * * *».

Побочная польза: нехватку денег AdStopAll заметит через час, а не через сутки.

Полностью в ноль это не выводит — у самого Директа статистика приходит с
задержкой, и «Показано» всегда чуть отстаёт от кабинета.

В отчёте приёмки записан замер рынка: цена в этом кабинете около 250-300 рублей
за тысячу показов при нашей прежней ставке 72, и потолок показов упирается не в
деньги, а в размер списка — кампания с бюджетом 13000 рублей за четыре дня
набрала 457 показов и потратила 138.

Прогон: 388 сторожей блока рекламы, статанализ 0, стиль чист. Схема не менялась.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:59:20 +03:00

64 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
use Illuminate\Console\Scheduling\Schedule;
it('registers advertising Direct jobs in the scheduler', function () {
$events = app(Schedule::class)->events();
$summaries = array_map(fn ($e) => $e->getSummaryForDisplay(), $events);
$joined = implode("\n", $summaries);
expect($joined)->toContain('ChargeCampaignSpendJob')
->and($joined)->toContain('SyncCampaignAudienceJob')
->and($joined)->toContain('SyncCampaignModerationJob');
});
/**
* Как часто портал спрашивает Яндекс о судьбе объявлений.
*
* 🔴 Было «раз в два часа», и клиент столько же времени видел на карточке «На модерации»,
* когда Яндекс уже всё одобрил и объявления шли. Поймано владельцем 04.08.2026 на живой
* кампании #11: проверка кончилась, а портал об этом узнал бы только через полтора часа.
* Обход дешёвый — чтение по кампаниям, ждущим вердикта, ничего в кабинете не меняет.
*/
/**
* Как часто портал узнаёт, сколько показов уже сделано.
*
* 🔴 Было «раз в сутки в 04:20», и это единственное место, где обновляется число
* показов на карточке. Кампанию #11 запустили в 04:39 — на двадцать минут позже
* обхода, и клиент до следующего утра видел «Показано 0», когда показы уже шли.
* Поймано владельцем 04.08.2026: «показано 0, почему? там один же есть».
*
* Списывать чаще безопасно по устройству: счётчик идемпотентен по ключу
* «yandex-imp:{кампания}:{показы}» и монотонен — списывается только прирост.
*/
it('узнаёт число показов каждый час, а не раз в сутки', function () {
$event = collect(app(Schedule::class)->events())
->first(fn ($e) => str_contains($e->getSummaryForDisplay(), 'ChargeCampaignSpendJob'));
expect($event)->not->toBeNull()
->and($event->expression)->toBe('0 * * * *');
});
it('спрашивает Яндекс о модерации каждые 15 минут', function () {
$event = collect(app(Schedule::class)->events())
->first(fn ($e) => str_contains($e->getSummaryForDisplay(), 'SyncCampaignModerationJob'));
expect($event)->not->toBeNull()
->and($event->expression)->toBe('*/15 * * * *');
});
/**
* Сторож очереди робота обязан быть в расписании.
*
* Сама команда без расписания бесполезна: зависшее «в работе» задание держит очередь для
* ВСЕХ клиентов, а разбирать пробку руками некому — робот работает по ночам без людей.
*/
it('registers the creative job reaper in the scheduler', function () {
$events = app(Schedule::class)->events();
$joined = implode("\n", array_map(fn ($e) => $e->getSummaryForDisplay(), $events));
expect($joined)->toContain('creative-jobs:reap');
});