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'); });