168 lines
6.0 KiB
PHP
168 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Exceptions\Advertising\AudienceTooSmallException;
|
|
use App\Models\AdCampaign;
|
|
use App\Models\AdCampaignAd;
|
|
use App\Models\AdWallet;
|
|
use App\Models\AdWalletHold;
|
|
use App\Models\Tenant;
|
|
use App\Services\Advertising\AdWalletService;
|
|
use App\Services\Advertising\CampaignLauncher;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
function fakeYandexEndpoints(): void
|
|
{
|
|
Http::fake([
|
|
'*/segments/upload_csv_file' => 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();
|
|
});
|