create(); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => false, 'status' => AdCampaign::STATUS_REJECTED, 'yandex_campaign_id' => 555, 'yandex_ad_group_id' => 666, 'yandex_segment_id' => 777, 'moderation_reason' => 'Изображение не подошло', ]); $banner = AdCampaignBanner::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'width' => 300, 'height' => 250, 'bytes' => 1000, 'included' => true, 'path' => 'ad-banners/a/300x250.jpg', 'yandex_creative_id' => 100, 'yandex_ad_id' => 200, 'moderation_status' => AdCampaignBanner::MOD_REJECTED, ]); return [$tenant, $campaign, $banner]; } it('оживление возвращает кампанию в черновик и чистит номера отклонённых объявлений', function () { config(['services.yandex_direct.enabled' => true]); Http::fake(['*' => Http::response(['result' => ['DeleteResults' => [['Id' => 200]]]])]); [, $campaign, $banner] = rejectedCampaign(); app(CampaignReviveService::class)->revive($campaign); expect($campaign->refresh()->status)->toBe(AdCampaign::STATUS_DRAFT) ->and($banner->refresh()->yandex_creative_id)->toBeNull() ->and($banner->refresh()->yandex_ad_id)->toBeNull(); }); /** * Номера кампании, группы и сегмента — то, на чём держится возобновляемый запуск. * Стереть их — значит завести в Яндексе второй комплект всего и заплатить дважды. */ it('номера кампании, группы и сегмента при оживлении уцелевают', function () { config(['services.yandex_direct.enabled' => true]); Http::fake(['*' => Http::response(['result' => ['DeleteResults' => [['Id' => 200]]]])]); [, $campaign] = rejectedCampaign(); app(CampaignReviveService::class)->revive($campaign); $fresh = $campaign->refresh(); expect((int) $fresh->yandex_campaign_id)->toBe(555) ->and((int) $fresh->yandex_ad_group_id)->toBe(666) ->and((int) $fresh->yandex_segment_id)->toBe(777); }); it('оживить можно только отклонённую кампанию', function () { [, $campaign] = rejectedCampaign(); $campaign->update(['status' => AdCampaign::STATUS_RUNNING]); expect(fn () => app(CampaignReviveService::class)->revive($campaign)) ->toThrow(RuntimeException::class); }); /** Яндекс не удалил объявление — это не повод не дать клиенту исправиться. */ it('неудача удаления в Яндексе не мешает оживлению', function () { config(['services.yandex_direct.enabled' => true]); Http::fake(['*' => Http::response(['error' => ['error_string' => 'нет такого']], 200)]); [, $campaign, $banner] = rejectedCampaign(); app(CampaignReviveService::class)->revive($campaign); expect($campaign->refresh()->status)->toBe(AdCampaign::STATUS_DRAFT) ->and($banner->refresh()->yandex_ad_id)->toBeNull(); }); it('при выключенном рубильнике в Яндекс не ходим вовсе', function () { config(['services.yandex_direct.enabled' => false]); Http::fake(); [, $campaign] = rejectedCampaign(); app(CampaignReviveService::class)->revive($campaign); Http::assertNothingSent(); expect($campaign->refresh()->status)->toBe(AdCampaign::STATUS_DRAFT); }); /** * Принятый баннер трогать нельзя: его объявление живо в кабинете и на нём держится * возобновляемый запуск. Обнулить ему номера — значит при следующем «Запустить» * завести в Яндексе второе такое же объявление. */ it('принятый баннер оживление не трогает', function () { config(['services.yandex_direct.enabled' => true]); Http::fake(['*' => Http::response(['result' => ['DeleteResults' => [['Id' => 200]]]])]); [$tenant, $campaign] = rejectedCampaign(); $accepted = AdCampaignBanner::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'width' => 728, 'height' => 90, 'bytes' => 1000, 'included' => true, 'path' => 'ad-banners/a/728x90.jpg', 'yandex_creative_id' => 300, 'yandex_ad_id' => 400, 'moderation_status' => AdCampaignBanner::MOD_ACCEPTED, ]); app(CampaignReviveService::class)->revive($campaign); expect((int) $accepted->refresh()->yandex_creative_id)->toBe(300) ->and((int) $accepted->refresh()->yandex_ad_id)->toBe(400); // И в Яндекс на удаление ушёл только номер отклонённого объявления. Http::assertSent(fn ($req) => $req->data()['params']['SelectionCriteria']['Ids'] === [200]); });