2026-07-19 11:07:24 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use App\Jobs\SyncAdAudienceJob;
|
2026-07-23 12:03:29 +03:00
|
|
|
|
use App\Jobs\SyncVkAudienceJob;
|
2026-07-19 11:07:24 +03:00
|
|
|
|
use App\Models\SalesAdAudienceFirm;
|
|
|
|
|
|
use App\Models\SalesAdAudiencePhone;
|
2026-07-21 11:01:44 +03:00
|
|
|
|
use App\Models\SalesAdAudiencePlatform;
|
2026-07-19 11:07:24 +03:00
|
|
|
|
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']);
|
2026-07-21 11:01:44 +03:00
|
|
|
|
SalesAdAudiencePlatform::forCode('yandex')->update([
|
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
|
'external_id' => 58029600,
|
|
|
|
|
|
]);
|
2026-07-19 11:07:24 +03:00
|
|
|
|
});
|
2026-07-19 11:51:50 +03:00
|
|
|
|
|
|
|
|
|
|
it('номер без хозяина в рекламу не попадает', function () {
|
|
|
|
|
|
// Сирота из схемы до v8.77: фирмы у него нет, значит ночной пересчёт его
|
|
|
|
|
|
// никогда не тронет. Попади он в сегмент — крутился бы вечно.
|
|
|
|
|
|
Http::fake([
|
|
|
|
|
|
'api-audience.yandex.ru/*' => Http::response(['segment' => ['id' => 58029600]], 200),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-20 08:13:09 +03:00
|
|
|
|
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тестовая стоматология', 'ch_yandex' => true]);
|
2026-07-22 15:19:13 +03:00
|
|
|
|
$firm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => $firm->warmup_started_at ?? now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
2026-07-19 11:51:50 +03:00
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-19 11:07:24 +03:00
|
|
|
|
|
2026-07-23 19:20:15 +03:00
|
|
|
|
it('при нуле активных номеров не дёргает Яндекс и гасит прошлую ошибку', function () {
|
|
|
|
|
|
// После чистки данных активных номеров ноль. Пустую замену Яндекс отвергает
|
|
|
|
|
|
// 400-м «Нет ни одного корректного элемента», и на экране начальника повисает
|
|
|
|
|
|
// красная строчка. Слать нечего — джоб обязан тихо выйти, погасив прошлую
|
|
|
|
|
|
// ошибку. Сам сегмент в Яндексе при этом сохраняет прежний состав.
|
|
|
|
|
|
Http::fake();
|
|
|
|
|
|
SalesAdAudiencePlatform::forCode('yandex')->update([
|
|
|
|
|
|
'last_error' => 'Яндекс не принял обновление: Нет ни одного корректного элемента',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
SyncAdAudienceJob::dispatchSync();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertNothingSent();
|
|
|
|
|
|
expect(SalesAdAudiencePlatform::forCode('yandex')->last_error)->toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-19 11:07:24 +03:00
|
|
|
|
it('пока рубильник выключен, в Яндекс ничего не уходит', function () {
|
|
|
|
|
|
Http::fake();
|
2026-07-21 11:01:44 +03:00
|
|
|
|
SalesAdAudiencePlatform::forCode('yandex')->update(['enabled' => false]);
|
2026-07-19 11:07:24 +03:00
|
|
|
|
|
|
|
|
|
|
SyncAdAudienceJob::dispatchSync();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertNothingSent();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-22 11:00:49 +03:00
|
|
|
|
it('заливает номера фирмы, активной по срокам Яндекса, и пропускает погасшую', function () {
|
|
|
|
|
|
// Гранулярность теперь ПОФИРМЕННАЯ: решение движка прогрева считается на
|
|
|
|
|
|
// фирму по срокам Яндекса, а не по сводному phone.state (пересчёт пишет всем
|
|
|
|
|
|
// номерам фирмы один статус — разных статусов на одной фирме в жизни не бывает).
|
2026-07-19 11:07:24 +03:00
|
|
|
|
Http::fake([
|
|
|
|
|
|
'api-audience.yandex.ru/*' => Http::response(['segment' => ['id' => 58029600]], 200),
|
|
|
|
|
|
]);
|
2026-07-22 11:00:49 +03:00
|
|
|
|
SalesAdAudiencePlatform::forCode('yandex')->update(['warmup_days' => 3]);
|
2026-07-19 11:07:24 +03:00
|
|
|
|
|
2026-07-22 11:00:49 +03:00
|
|
|
|
// Активна по Яндексу: прогрев только начат (warmup_started_at = сейчас).
|
|
|
|
|
|
$fresh = SalesAdAudienceFirm::create([
|
|
|
|
|
|
'firm_name' => 'Свежая', 'warmup_started_at' => now(), 'ch_yandex' => true,
|
|
|
|
|
|
]);
|
2026-07-22 15:19:13 +03:00
|
|
|
|
$fresh->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
2026-07-22 11:00:49 +03:00
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000001', 'firm_id' => $fresh->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active',
|
|
|
|
|
|
]);
|
2026-07-19 11:07:24 +03:00
|
|
|
|
|
2026-07-22 11:00:49 +03:00
|
|
|
|
// Погасла по Яндексу: прогрев начат 10 дней назад > warmup_days=3.
|
|
|
|
|
|
$gone = SalesAdAudienceFirm::create([
|
|
|
|
|
|
'firm_name' => 'Погасшая', 'warmup_started_at' => now()->subDays(10), 'ch_yandex' => true,
|
|
|
|
|
|
]);
|
2026-07-22 15:19:13 +03:00
|
|
|
|
$gone->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now()->subDays(10), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
2026-07-22 11:00:49 +03:00
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000002', 'firm_id' => $gone->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active',
|
|
|
|
|
|
]);
|
2026-07-19 11:07:24 +03:00
|
|
|
|
|
|
|
|
|
|
SyncAdAudienceJob::dispatchSync();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSent(function (Request $request) {
|
2026-07-22 11:00:49 +03:00
|
|
|
|
$file = collect($request->data())->firstWhere('name', 'file');
|
|
|
|
|
|
$csv = is_array($file) && is_string($file['contents'] ?? null) ? $file['contents'] : (string) $request->body();
|
2026-07-19 11:07:24 +03:00
|
|
|
|
|
|
|
|
|
|
return str_contains($request->url(), 'modify_data')
|
2026-07-22 11:00:49 +03:00
|
|
|
|
&& str_contains($csv, '79990000001')
|
|
|
|
|
|
&& ! str_contains($csv, '79990000002');
|
2026-07-19 11:07:24 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-21 11:01:44 +03:00
|
|
|
|
expect(SalesAdAudiencePlatform::forCode('yandex')->last_synced_at)->not->toBeNull();
|
2026-07-19 11:07:24 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('ошибку Яндекса показывает начальнику, а не прячет', function () {
|
|
|
|
|
|
Http::fake([
|
|
|
|
|
|
'api-audience.yandex.ru/*' => Http::response(['error' => 'quota exceeded'], 429),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-23 19:20:15 +03:00
|
|
|
|
// Фирма реально греется по Яндексу (строка warming) — иначе номеров ноль,
|
|
|
|
|
|
// джоб тихо выйдет и до обращения к Яндексу не дойдёт (пустую заливку не шлём).
|
|
|
|
|
|
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тестовая стоматология', 'warmup_started_at' => now(), 'ch_yandex' => true]);
|
|
|
|
|
|
$firm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
2026-07-19 11:07:24 +03:00
|
|
|
|
SalesAdAudiencePhone::create(['phone' => '79990000001', 'firm_id' => $firm->id, 'expires_at' => now()->addDays(3), 'state' => 'active']);
|
|
|
|
|
|
|
|
|
|
|
|
SyncAdAudienceJob::dispatchSync();
|
|
|
|
|
|
|
2026-07-21 11:01:44 +03:00
|
|
|
|
expect(SalesAdAudiencePlatform::forCode('yandex')->last_error)->toContain('quota');
|
2026-07-19 11:07:24 +03:00
|
|
|
|
});
|
2026-07-19 19:30:10 +03:00
|
|
|
|
|
|
|
|
|
|
it('фирму, помеченную только для ВК, в Яндекс не льёт', function () {
|
|
|
|
|
|
// 🔴 Смысл фичи: начальник увёл фирму в ВК — из яндексового сегмента она
|
|
|
|
|
|
// должна уйти сама при ближайшей заливке, без ручной чистки.
|
|
|
|
|
|
config(['services.yandex_audience.token' => 'test-token']);
|
2026-07-21 11:01:44 +03:00
|
|
|
|
SalesAdAudiencePlatform::forCode('yandex')->update([
|
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
|
'external_id' => 58029600,
|
|
|
|
|
|
]);
|
2026-07-19 19:30:10 +03:00
|
|
|
|
|
|
|
|
|
|
$yandexFirm = SalesAdAudienceFirm::create([
|
2026-07-20 08:13:09 +03:00
|
|
|
|
'firm_name' => 'Греем в Яндексе', 'warmup_started_at' => now(), 'ch_yandex' => true,
|
2026-07-19 19:30:10 +03:00
|
|
|
|
]);
|
2026-07-22 15:19:13 +03:00
|
|
|
|
$yandexFirm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
2026-07-19 19:30:10 +03:00
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000001', 'firm_id' => $yandexFirm->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$vkFirm = SalesAdAudienceFirm::create([
|
2026-07-20 08:13:09 +03:00
|
|
|
|
'firm_name' => 'Греем в ВК', 'warmup_started_at' => now(), 'ch_vk' => true,
|
2026-07-19 19:30:10 +03:00
|
|
|
|
]);
|
2026-07-22 15:19:13 +03:00
|
|
|
|
$vkFirm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'vk', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
2026-07-19 19:30:10 +03:00
|
|
|
|
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();
|
|
|
|
|
|
|
2026-07-19 20:30:33 +03:00
|
|
|
|
Http::assertSent(function (Request $r) {
|
2026-07-19 19:30:10 +03:00
|
|
|
|
$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;
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-22 11:00:49 +03:00
|
|
|
|
|
|
|
|
|
|
it('фирма, погасшая по срокам Яндекса, в сегмент не входит — даже если сводка active из-за ВК', function () {
|
|
|
|
|
|
config(['services.yandex_audience.token' => 'test-token']);
|
|
|
|
|
|
SalesAdAudiencePlatform::forCode('yandex')->update(['enabled' => true, 'external_id' => 58029600, 'warmup_days' => 3]);
|
|
|
|
|
|
SalesAdAudiencePlatform::forCode('vk')->update(['warmup_days' => 30]);
|
|
|
|
|
|
Http::fake(['api-audience.yandex.ru/*' => Http::response(['result' => true], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
// Фирма на ОБЕИХ площадках, прогрев начат 5 дней назад:
|
|
|
|
|
|
// по Яндексу (warmup 3) уже погасла, по ВК (warmup 30) ещё active.
|
|
|
|
|
|
// Сводный phone.state будет 'active' (из-за ВК) — но в ЯНДЕКС номер не должен уйти.
|
|
|
|
|
|
$firm = SalesAdAudienceFirm::create([
|
|
|
|
|
|
'firm_name' => 'На обеих', 'warmup_started_at' => now()->subDays(5),
|
|
|
|
|
|
'ch_yandex' => true, 'ch_vk' => true,
|
|
|
|
|
|
]);
|
2026-07-22 15:19:13 +03:00
|
|
|
|
$firm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now()->subDays(5), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
|
|
|
|
|
$firm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'vk', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now()->subDays(5), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
2026-07-22 11:00:49 +03:00
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000077', 'firm_id' => $firm->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active', // сводка active
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-23 19:20:15 +03:00
|
|
|
|
// Живая по Яндексу фирма рядом — чтобы заливка реально ушла (пустую не шлём),
|
|
|
|
|
|
// и исключение погасшей проверялось на настоящем составе, а не на тишине.
|
|
|
|
|
|
$live = SalesAdAudienceFirm::create(['firm_name' => 'Живая', 'warmup_started_at' => now(), 'ch_yandex' => true]);
|
|
|
|
|
|
$live->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
|
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000078', 'firm_id' => $live->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-22 11:00:49 +03:00
|
|
|
|
SyncAdAudienceJob::dispatchSync();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSent(function (Request $r) {
|
|
|
|
|
|
$file = collect($r->data())->firstWhere('name', 'file');
|
|
|
|
|
|
$csv = is_array($file) && is_string($file['contents'] ?? null) ? $file['contents'] : (string) $r->body();
|
2026-07-23 19:20:15 +03:00
|
|
|
|
expect($csv)->toContain('79990000078')
|
|
|
|
|
|
->and($csv)->not->toContain('79990000077');
|
2026-07-22 11:00:49 +03:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-22 15:19:13 +03:00
|
|
|
|
|
|
|
|
|
|
it('фирма со строкой yandex warming+active — номер попадает в сегмент (Task 5)', function () {
|
|
|
|
|
|
// Источник Task 5: не forYandex()/ch_yandex, а строка firm_channels
|
|
|
|
|
|
// (channel=yandex, status=warming). Решение по ней же — decideForChannelRow.
|
|
|
|
|
|
Http::fake(['api-audience.yandex.ru/*' => Http::response(['result' => true], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тёплая по Яндексу', 'ch_yandex' => true]);
|
|
|
|
|
|
$firm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
|
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000031', 'firm_id' => $firm->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
SyncAdAudienceJob::dispatchSync();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSent(function (Request $r) {
|
|
|
|
|
|
$file = collect($r->data())->firstWhere('name', 'file');
|
|
|
|
|
|
$csv = is_array($file) && is_string($file['contents'] ?? null) ? $file['contents'] : (string) $r->body();
|
|
|
|
|
|
|
|
|
|
|
|
return str_contains($r->url(), 'modify_data') && str_contains($csv, '79990000031');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('фирма со строкой yandex loaded (не греется) — номер НЕ в сегменте, даже с ch_yandex=true (Task 5)', function () {
|
|
|
|
|
|
Http::fake(['api-audience.yandex.ru/*' => Http::response(['result' => true], 200)]);
|
|
|
|
|
|
|
|
|
|
|
|
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Уже загруженная', 'ch_yandex' => true]);
|
|
|
|
|
|
$firm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'loaded', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now()->subDays(20), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
|
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000032', 'firm_id' => $firm->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-23 19:20:15 +03:00
|
|
|
|
// Живая (warming) фирма рядом — чтобы заливка ушла и «loaded» проверялся на
|
|
|
|
|
|
// настоящем составе: пустую заливку джоб больше не шлёт.
|
|
|
|
|
|
$warm = SalesAdAudienceFirm::create(['firm_name' => 'Греется', 'ch_yandex' => true]);
|
|
|
|
|
|
$warm->firmChannels()->create([
|
|
|
|
|
|
'channel' => 'yandex', 'status' => 'warming', 'mode' => 'funnel',
|
|
|
|
|
|
'warming_started_at' => now(), 'warmed_times' => 1,
|
|
|
|
|
|
]);
|
|
|
|
|
|
SalesAdAudiencePhone::create([
|
|
|
|
|
|
'phone' => '79990000033', 'firm_id' => $warm->id,
|
|
|
|
|
|
'expires_at' => now()->addDays(30), 'state' => 'active',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-07-22 15:19:13 +03:00
|
|
|
|
SyncAdAudienceJob::dispatchSync();
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSent(function (Request $r) {
|
|
|
|
|
|
$file = collect($r->data())->firstWhere('name', 'file');
|
|
|
|
|
|
$csv = is_array($file) && is_string($file['contents'] ?? null) ? $file['contents'] : (string) $r->body();
|
|
|
|
|
|
|
2026-07-23 19:20:15 +03:00
|
|
|
|
return str_contains($r->url(), 'modify_data')
|
|
|
|
|
|
&& str_contains($csv, '79990000033')
|
|
|
|
|
|
&& ! str_contains($csv, '79990000032');
|
2026-07-22 15:19:13 +03:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-23 12:03:29 +03:00
|
|
|
|
|
|
|
|
|
|
it('VK-джоб пишет статус доступа в СТРОКУ ПЛОЩАДКИ, а не в мёртвый синглтон (экран читает строку)', function () {
|
|
|
|
|
|
// Экран ВК показывает status из строки sales_ad_audience_platforms (platformState),
|
|
|
|
|
|
// поэтому джоб обязан обновлять именно её. До Фазы 2 статус уходил в SalesAdAudienceState.
|
|
|
|
|
|
config(['services.vk_ads.token' => 'test-token']);
|
|
|
|
|
|
SalesAdAudiencePlatform::forCode('vk')->update(['enabled' => true, 'min_phones' => 2000]);
|
|
|
|
|
|
|
|
|
|
|
|
// Номеров нет → путь waiting_volume (без обращения к API ВК).
|
|
|
|
|
|
SyncVkAudienceJob::dispatchSync();
|
|
|
|
|
|
|
|
|
|
|
|
expect(SalesAdAudiencePlatform::forCode('vk')->status)->toBe('waiting_volume');
|
|
|
|
|
|
});
|