Files
portal/app/tests/Feature/Sales/AdAudienceSyncTest.php
T
Дмитрий 53d2165b25 feat(sales): ночная заливка состава аудитории в Яндекс
SyncAdAudienceJob использует готовый YandexAudienceClient (Http::fake
в тестах) и режим modify_data replace: заливает весь активный список
целиком раз в сутки (03:20 МСК, сразу после RecalcAdAudienceJob).
Рубильник sales_ad_audience_state.enabled=false — джоб не делает ни
одного обращения к Яндексу. Ошибку Яндекса кладём в last_error,
не проглатываем молча.

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

77 lines
3.2 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 () {
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');
});