Files
portal/app/tests/Feature/Sales/AdAudienceSyncTest.php
T
Дмитрий c6137b910d style(sales): автоформатирование Pint в файлах рекламной аудитории
Хвосты от pre-commit хука: импорт Builder и сокращение полных имён
классов в PHPDoc. Поведение не меняется, тесты те же.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:30:33 +03:00

135 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\SyncAdAudienceJob;
use App\Models\SalesAdAudienceFirm;
use App\Models\SalesAdAudiencePhone;
use App\Models\SalesAdAudienceState;
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']);
SalesAdAudienceState::current()->update(['enabled' => true, 'yandex_segment_id' => 58029600]);
});
it('номер без хозяина в рекламу не попадает', function () {
// Сирота из схемы до v8.77: фирмы у него нет, значит ночной пересчёт его
// никогда не тронет. Попади он в сегмент — крутился бы вечно.
Http::fake([
'api-audience.yandex.ru/*' => Http::response(['segment' => ['id' => 58029600]], 200),
]);
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тестовая стоматология']);
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();
SalesAdAudienceState::current()->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' => 'Тестовая стоматология']);
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(SalesAdAudienceState::current()->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(SalesAdAudienceState::current()->last_error)->toContain('quota');
});
it('фирму, помеченную только для ВК, в Яндекс не льёт', function () {
// 🔴 Смысл фичи: начальник увёл фирму в ВК — из яндексового сегмента она
// должна уйти сама при ближайшей заливке, без ручной чистки.
config(['services.yandex_audience.token' => 'test-token']);
SalesAdAudienceState::current()->update(['enabled' => true, 'yandex_segment_id' => 58029600]);
$yandexFirm = SalesAdAudienceFirm::create([
'firm_name' => 'Греем в Яндексе', 'warmup_started_at' => now(), 'channels' => 'yandex',
]);
SalesAdAudiencePhone::create([
'phone' => '79990000001', 'firm_id' => $yandexFirm->id,
'expires_at' => now()->addDays(30), 'state' => 'active',
]);
$vkFirm = SalesAdAudienceFirm::create([
'firm_name' => 'Греем в ВК', 'warmup_started_at' => now(), 'channels' => 'vk',
]);
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;
});
});