Http::response(['segment' => ['id' => 900001]]), '*/segment/*/confirm' => Http::response(['segment' => ['id' => 900001]]), '*/json/v5/retargetinglists' => Http::response(['result' => ['AddResults' => [['Id' => 111]]]]), '*/json/v5/campaigns' => Http::response(['result' => ['AddResults' => [['Id' => 222]]]]), '*/json/v5/adgroups' => Http::response(['result' => ['AddResults' => [['Id' => 333]]]]), '*/json/v5/audiencetargets' => Http::response(['result' => ['AddResults' => [['Id' => 444]]]]), '*/json/v5/ads' => Http::response(['result' => ['AddResults' => [['Id' => 555]]]]), ]); } function configureYandex(): void { config(['services.yandex_direct.enabled' => true]); config(['services.yandex_direct.token' => 'DIRTOKEN']); config(['services.yandex_direct.base_url' => 'https://api-sandbox.direct.yandex.com']); config(['services.yandex_audience.token' => 'AUDTOKEN']); } /** Наполняет ad_campaign_phones $count уникальными номерами для кампании (обходит фабрику Deal — быстрее). */ function seedAudience(AdCampaign $campaign, int $count): void { $rows = []; for ($i = 0; $i < $count; $i++) { $rows[] = [ 'tenant_id' => $campaign->tenant_id, 'campaign_id' => $campaign->id, 'phone' => sprintf('799900%05d', $i), 'expires_at' => null, 'created_at' => now(), 'updated_at' => now(), ]; } DB::table('ad_campaign_phones')->insert($rows); } it('launches a campaign: segment → Direct → freeze budget → pending moderation', function () { configureYandex(); fakeYandexEndpoints(); $tenant = Tenant::factory()->create(); app(AdWalletService::class)->topup($tenant->id, '5000.00', 'yandex', 'тест'); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => true, 'weekly_budget_rub' => '2600.00', 'click_bid_rub' => '13.00', ]); $ad = AdCampaignAd::create([ 'tenant_id' => $tenant->id, 'campaign_id' => $campaign->id, 'title' => 'Заголовок', 'text' => 'Текст объявления', 'href' => 'https://liderra.ru', 'moderation_status' => 'draft', ]); seedAudience($campaign, 100); app(CampaignLauncher::class)->launch($campaign); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_PENDING_MODERATION) ->and($campaign->yandex_segment_id)->toBe(900001) ->and($campaign->yandex_retargeting_list_id)->toBe(111) ->and($campaign->yandex_campaign_id)->toBe(222) ->and($campaign->yandex_ad_group_id)->toBe(333) ->and($campaign->launched_at)->not->toBeNull(); $ad->refresh(); expect($ad->yandex_ad_id)->toBe(555) ->and($ad->moderation_status)->toBe('MODERATION'); $wallet = AdWallet::where('tenant_id', $tenant->id)->first(); expect($wallet->frozen_rub)->toBe('2600.00'); $hold = AdWalletHold::where('tenant_id', $tenant->id) ->where('channel', 'yandex')->where('source_type', 'campaign') ->where('source_id', $campaign->id)->where('status', AdWalletHold::STATUS_ACTIVE)->first(); expect($hold)->not->toBeNull() ->and($hold->amount_rub)->toBe('2600.00'); Http::assertSent(function ($request) { if (! str_contains($request->url(), '/json/v5/campaigns')) { return false; } $limit = $request['params']['Campaigns'][0]['TextCampaign']['BiddingStrategy']['Network']['NetworkHighestPosition']['WeeklySpendLimit'] ?? null; return $limit === 2000000000; }); }); it('throws AudienceTooSmallException and does not freeze when audience is under 100', function () { configureYandex(); fakeYandexEndpoints(); $tenant = Tenant::factory()->create(); app(AdWalletService::class)->topup($tenant->id, '5000.00', 'yandex', 'тест'); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => true, 'weekly_budget_rub' => '2600.00', 'click_bid_rub' => '13.00', ]); seedAudience($campaign, 50); expect(fn () => app(CampaignLauncher::class)->launch($campaign)) ->toThrow(AudienceTooSmallException::class); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_DRAFT); $wallet = AdWallet::where('tenant_id', $tenant->id)->first(); expect($wallet->frozen_rub)->toBe('0.00'); $holdExists = AdWalletHold::where('tenant_id', $tenant->id) ->where('source_type', 'campaign')->where('source_id', $campaign->id) ->where('status', AdWalletHold::STATUS_ACTIVE)->exists(); expect($holdExists)->toBeFalse(); }); it('throws RuntimeException when yandex_direct is disabled and creates nothing', function () { config(['services.yandex_direct.enabled' => false]); fakeYandexEndpoints(); $tenant = Tenant::factory()->create(); $campaign = AdCampaign::create([ 'tenant_id' => $tenant->id, 'name' => 'C', 'audience_days' => 10, 'use_uploaded_list' => false, 'weekly_budget_rub' => '2600.00', 'click_bid_rub' => '13.00', ]); expect(fn () => app(CampaignLauncher::class)->launch($campaign)) ->toThrow(RuntimeException::class); $campaign->refresh(); expect($campaign->status)->toBe(AdCampaign::STATUS_DRAFT) ->and($campaign->yandex_segment_id)->toBeNull(); Http::assertNothingSent(); });