true, 'services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com', 'services.yandex_direct.token' => 'DIRTOKEN', ]); Http::fake([ '*/json/v5/ads' => Http::response(['result' => ['Ads' => [ ['Id' => 555, 'Status' => 'REJECTED', 'State' => 'OFF', 'StatusClarification' => 'Нет гарантий'], ]]], 200), ]); $tenant = Tenant::factory()->create(); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'Кампания на модерации', 'status' => AdCampaign::STATUS_PENDING_MODERATION, 'yandex_campaign_id' => 222, 'weekly_budget_rub' => '500.00', 'audience_days' => 10, 'use_uploaded_list' => true, ]); $ad = AdCampaignAd::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'title' => 'Заголовок', 'text' => 'Текст объявления', 'href' => 'https://example.test', 'yandex_ad_id' => 555, 'moderation_status' => 'MODERATION', ]); app(SyncCampaignModerationJob::class)->handle(); $ad->refresh(); expect($ad->moderation_status)->toBe('REJECTED'); expect($ad->moderation_reason)->toBe('Нет гарантий'); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_REJECTED); }); it('модерация: все ACCEPTED переводит кампанию в running (passthrough)', function () { config([ 'services.yandex_direct.enabled' => true, 'services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com', 'services.yandex_direct.token' => 'DIRTOKEN', ]); Http::fake([ '*/json/v5/ads' => Http::response(['result' => ['Ads' => [ ['Id' => 555, 'Status' => 'ACCEPTED', 'State' => 'ON', 'StatusClarification' => null], ]]], 200), ]); $tenant = Tenant::factory()->create(); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'Кампания на модерации 2', 'status' => AdCampaign::STATUS_PENDING_MODERATION, 'yandex_campaign_id' => 223, 'weekly_budget_rub' => '500.00', 'audience_days' => 10, 'use_uploaded_list' => true, ]); $ad = AdCampaignAd::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'title' => 'Заголовок', 'text' => 'Текст объявления', 'href' => 'https://example.test', 'yandex_ad_id' => 555, 'moderation_status' => 'MODERATION', ]); app(SyncCampaignModerationJob::class)->handle(); $ad->refresh(); expect($ad->moderation_status)->toBe('ACCEPTED'); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_RUNNING); }); it('AdvertisingStopped ставит кампанию тенанта на паузу через реальный слушатель', function () { config([ 'services.yandex_direct.enabled' => true, 'services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com', 'services.yandex_direct.token' => 'DIRTOKEN', ]); Http::fake([ '*/json/v5/campaigns' => Http::response(['result' => []], 200), ]); $tenant = Tenant::factory()->create(); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'Кампания в работе', 'status' => AdCampaign::STATUS_RUNNING, 'yandex_campaign_id' => 224, 'weekly_budget_rub' => '500.00', 'audience_days' => 10, 'use_uploaded_list' => true, ]); // Без Event::fake — доказываем, что слушатель реально ЗАРЕГИСТРИРОВАН и сработал. event(new AdvertisingStopped($tenant->id)); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_STOPPED_NO_FUNDS); Http::assertSent(function ($request) { return str_contains($request->url(), '/json/v5/campaigns') && ($request->data()['method'] ?? null) === 'suspend'; }); }); it('AdvertisingStopped не трогает кампанию чужого тенанта (явный tenant-фильтр)', function () { config([ 'services.yandex_direct.enabled' => true, 'services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com', 'services.yandex_direct.token' => 'DIRTOKEN', ]); Http::fake([ '*/json/v5/campaigns' => Http::response(['result' => []], 200), ]); $tenantA = Tenant::factory()->create(); $tenantB = Tenant::factory()->create(); $campaignA = AdCampaign::create([ 'tenant_id' => $tenantA->id, 'name' => 'Кампания тенанта A', 'status' => AdCampaign::STATUS_RUNNING, 'yandex_campaign_id' => 225, 'weekly_budget_rub' => '500.00', 'audience_days' => 10, 'use_uploaded_list' => true, ]); $campaignB = AdCampaign::create([ 'tenant_id' => $tenantB->id, 'name' => 'Кампания тенанта B', 'status' => AdCampaign::STATUS_RUNNING, 'yandex_campaign_id' => 226, 'weekly_budget_rub' => '500.00', 'audience_days' => 10, 'use_uploaded_list' => true, ]); event(new AdvertisingStopped($tenantA->id)); $campaignA->refresh(); $campaignB->refresh(); expect($campaignA->status)->toBe(AdCampaign::STATUS_STOPPED_NO_FUNDS); expect($campaignB->status)->toBe(AdCampaign::STATUS_RUNNING); }); // ВЫХОД 2 — Яндекс отклонил объявление. Показов не было, деньги не тратились → // заморозка возвращается клиенту полностью. it('ВЫХОД 2: отклонение модерацией снимает заморозку кампании', function () { config([ 'services.yandex_direct.enabled' => true, 'services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com', 'services.yandex_direct.token' => 'DIRTOKEN', ]); Http::fake([ '*/json/v5/ads' => Http::response(['result' => ['Ads' => [ ['Id' => 557, 'Status' => 'REJECTED', 'State' => 'OFF', 'StatusClarification' => 'Не та тематика'], ]]], 200), ]); $tenant = Tenant::factory()->create(); $svc = app(AdWalletService::class); $svc->topup($tenant->id, '3000.00', 'yandex', 'тест'); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'Кампания под отказ', 'status' => AdCampaign::STATUS_PENDING_MODERATION, 'yandex_campaign_id' => 227, 'weekly_budget_rub' => '500.00', 'audience_days' => 10, 'use_uploaded_list' => true, ]); AdCampaignAd::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'title' => 'Заголовок', 'text' => 'Текст объявления', 'href' => 'https://example.test', 'yandex_ad_id' => 557, 'moderation_status' => 'MODERATION', ]); $svc->freeze($tenant->id, 'yandex', 'campaign', $campaign->id, '1200.00'); app(SyncCampaignModerationJob::class)->handle(); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_REJECTED) ->and(AdWallet::where('tenant_id', $tenant->id)->first()->frozen_rub)->toBe('0.00'); }); // ВЫХОД 3 — деньги кончились, вся реклама тенанта заглушена. Держать заморозку // на мёртвой кампании бессмысленно — она только занижает свободный остаток. it('ВЫХОД 3: остановка без средств снимает заморозку кампании', function () { config([ 'services.yandex_direct.enabled' => true, 'services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com', 'services.yandex_direct.token' => 'DIRTOKEN', ]); Http::fake([ '*/json/v5/campaigns' => Http::response(['result' => []], 200), ]); $tenant = Tenant::factory()->create(); $svc = app(AdWalletService::class); $svc->topup($tenant->id, '3000.00', 'yandex', 'тест'); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'Кампания без денег', 'status' => AdCampaign::STATUS_RUNNING, 'yandex_campaign_id' => 228, 'weekly_budget_rub' => '500.00', 'audience_days' => 10, 'use_uploaded_list' => true, ]); $svc->freeze($tenant->id, 'yandex', 'campaign', $campaign->id, '1200.00'); event(new AdvertisingStopped($tenant->id)); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_STOPPED_NO_FUNDS) ->and(AdWallet::where('tenant_id', $tenant->id)->first()->frozen_rub)->toBe('0.00'); });