Files
portal/app/tests/Feature/Sales/AdAudienceSyncTest.php
T
Дмитрий a04662898e fix(sales): джоб Яндекса читает/пишет рубильник и сегмент из sales_ad_audience_platforms
Task 3 переезда на per-platform таблицу: SyncAdAudienceJob больше не трогает
sales_ad_audience_state.enabled/yandex_segment_id — источник теперь строка
площадки 'yandex' в sales_ad_audience_platforms (enabled/external_id/
last_synced_at/last_error). Поведение не изменилось.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:01:44 +03:00

141 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\SyncAdAudienceJob;
use App\Models\SalesAdAudienceFirm;
use App\Models\SalesAdAudiencePhone;
use App\Models\SalesAdAudiencePlatform;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\Concerns\SharesSupplierPdo;
/**
* Ночная заливка состава аудитории в Яндекс (Task 6).
*
* SyncAdAudienceJob использует уже готовый YandexAudienceClient (тесты — в
* tests/Unit/YandexAudienceClientTest.php, здесь его не трогаем) и режим
* modify_data(..., 'replace'): заливает весь активный список целиком.
*
* Ключевое правило владельца: пока рубильник `enabled` выключен — в Яндекс
* не уходит НИЧЕГО (защита от случайной траты денег).
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
beforeEach(function () {
config(['services.yandex_audience.token' => 'test-token']);
SalesAdAudiencePlatform::forCode('yandex')->update([
'enabled' => true,
'external_id' => 58029600,
]);
});
it('номер без хозяина в рекламу не попадает', function () {
// Сирота из схемы до v8.77: фирмы у него нет, значит ночной пересчёт его
// никогда не тронет. Попади он в сегмент — крутился бы вечно.
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['segment' => ['id' => 58029600]], 200),
]);
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тестовая стоматология', 'ch_yandex' => true]);
SalesAdAudiencePhone::create(['phone' => '79990000001', 'firm_id' => $firm->id, 'expires_at' => now()->addDays(3), 'state' => 'active']);
SalesAdAudiencePhone::create(['phone' => '79990000009', 'firm_id' => null, 'expires_at' => now()->addDays(3), 'state' => 'active']);
SyncAdAudienceJob::dispatchSync();
Http::assertSent(function (Request $request) {
$body = (string) $request->body();
return str_contains($body, '79990000001') && ! str_contains($body, '79990000009');
});
});
it('пока рубильник выключен, в Яндекс ничего не уходит', function () {
Http::fake();
SalesAdAudiencePlatform::forCode('yandex')->update(['enabled' => false]);
SyncAdAudienceJob::dispatchSync();
Http::assertNothingSent();
});
it('заливает только активные номера', function () {
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['segment' => ['id' => 58029600]], 200),
]);
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тестовая стоматология', 'ch_yandex' => true]);
SalesAdAudiencePhone::create(['phone' => '79990000001', 'firm_id' => $firm->id, 'expires_at' => now()->addDays(3), 'state' => 'active']);
SalesAdAudiencePhone::create(['phone' => '79990000002', 'firm_id' => $firm->id, 'expires_at' => now()->addDays(3), 'state' => 'paused']);
SalesAdAudiencePhone::create(['phone' => '79990000003', 'firm_id' => $firm->id, 'expires_at' => now()->addDays(3), 'state' => 'stopped']);
SyncAdAudienceJob::dispatchSync();
Http::assertSent(function (Request $request) {
$body = (string) $request->body();
return str_contains($request->url(), 'modify_data')
&& str_contains($body, '79990000001')
&& ! str_contains($body, '79990000002')
&& ! str_contains($body, '79990000003');
});
expect(SalesAdAudiencePlatform::forCode('yandex')->last_synced_at)->not->toBeNull();
});
it('ошибку Яндекса показывает начальнику, а не прячет', function () {
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['error' => 'quota exceeded'], 429),
]);
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тестовая стоматология']);
SalesAdAudiencePhone::create(['phone' => '79990000001', 'firm_id' => $firm->id, 'expires_at' => now()->addDays(3), 'state' => 'active']);
SyncAdAudienceJob::dispatchSync();
expect(SalesAdAudiencePlatform::forCode('yandex')->last_error)->toContain('quota');
});
it('фирму, помеченную только для ВК, в Яндекс не льёт', function () {
// 🔴 Смысл фичи: начальник увёл фирму в ВК — из яндексового сегмента она
// должна уйти сама при ближайшей заливке, без ручной чистки.
config(['services.yandex_audience.token' => 'test-token']);
SalesAdAudiencePlatform::forCode('yandex')->update([
'enabled' => true,
'external_id' => 58029600,
]);
$yandexFirm = SalesAdAudienceFirm::create([
'firm_name' => 'Греем в Яндексе', 'warmup_started_at' => now(), 'ch_yandex' => true,
]);
SalesAdAudiencePhone::create([
'phone' => '79990000001', 'firm_id' => $yandexFirm->id,
'expires_at' => now()->addDays(30), 'state' => 'active',
]);
$vkFirm = SalesAdAudienceFirm::create([
'firm_name' => 'Греем в ВК', 'warmup_started_at' => now(), 'ch_vk' => true,
]);
SalesAdAudiencePhone::create([
'phone' => '79990000002', 'firm_id' => $vkFirm->id,
'expires_at' => now()->addDays(30), 'state' => 'active',
]);
Http::fake(['api-audience.yandex.ru/*' => Http::response(['result' => true], 200)]);
SyncAdAudienceJob::dispatchSync();
Http::assertSent(function (Request $r) {
$body = collect($r->data())->firstWhere('name', 'file');
$csv = is_array($body) && is_string($body['contents'] ?? null) ? $body['contents'] : '';
expect($csv)->toContain('79990000001')
->and($csv)->not->toContain('79990000002');
return true;
});
});