150 lines
8.0 KiB
PHP
150 lines
8.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Jobs\SyncVkAudienceJob;
|
||
use App\Models\SalesAdAudienceFirm;
|
||
use App\Models\SalesAdAudiencePhone;
|
||
use App\Models\SalesAdAudiencePlatform;
|
||
use App\Models\SalesAdAudienceState;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
|
||
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||
|
||
beforeEach(function () {
|
||
// Реклама по умолчанию выключена (enabled=false в схеме). Рубильник и порог
|
||
// ВК теперь живут на СТРОКЕ ПЛОЩАДКИ (sales_ad_audience_platforms, vk), а не на
|
||
// синглтоне sales_ad_audience_state — каждая площадка независима. Каждый тест
|
||
// канала включает ВК явно, иначе джоб честно выходит на своём рубильнике и мы
|
||
// бы проверяли не то, что думаем.
|
||
SalesAdAudiencePlatform::forCode('vk')->update(['enabled' => true]);
|
||
});
|
||
|
||
it('когда реклама выключена рубильником, джоб вообще не работает', function () {
|
||
// 🔴 Рубильник ВК (строка площадки vk.enabled) обязан гасить площадку. Он
|
||
// теперь свой у каждой площадки — можно погасить ВК, не трогая Яндекс, — но
|
||
// защитный смысл ТОТ ЖЕ: выключатель ВК в ВЫКЛ ⇒ джоб не делает ни одного
|
||
// обращения к ВК и статус канала остаётся нетронутым.
|
||
//
|
||
// 🪤 Проверять здесь Http::assertNothingSent() БЕСПОЛЕЗНО: при трёх номерах
|
||
// джоб и без рубильника не дошёл бы до обращения к ВК, и тест был бы зелёным
|
||
// в любом случае (проверено 19.07 — первая редакция теста проходила и с
|
||
// вырезанной защитой). Наблюдаемая разница одна: с рубильником джоб выходит
|
||
// сразу и статус канала остаётся нетронутым, без рубильника — он успевает
|
||
// проставить 'waiting_volume'.
|
||
SalesAdAudiencePlatform::forCode('vk')->update(['enabled' => false]);
|
||
SalesAdAudienceState::current()->update(['vk_status' => 'no_access']);
|
||
config(['services.vk_ads.token' => 'test-token']);
|
||
vkFirmWithPhones(3);
|
||
|
||
SyncVkAudienceJob::dispatchSync();
|
||
|
||
expect(SalesAdAudienceState::current()->vk_status)->toBe('no_access');
|
||
});
|
||
|
||
/** Заводит фирму для ВК с нужным числом номеров. */
|
||
function vkFirmWithPhones(int $count): SalesAdAudienceFirm
|
||
{
|
||
$firm = SalesAdAudienceFirm::create([
|
||
'firm_name' => 'Тестовая стоматология',
|
||
'warmup_started_at' => now(),
|
||
'ch_vk' => true,
|
||
]);
|
||
|
||
for ($i = 1; $i <= $count; $i++) {
|
||
SalesAdAudiencePhone::create([
|
||
'phone' => '7999'.str_pad((string) $i, 7, '0', STR_PAD_LEFT),
|
||
'firm_id' => $firm->id,
|
||
'expires_at' => now()->addDays(30),
|
||
'state' => 'active',
|
||
]);
|
||
}
|
||
|
||
return $firm;
|
||
}
|
||
|
||
it('без ключей доступа не трогает ВК и пишет «нет доступа»', function () {
|
||
config(['services.vk_ads.token' => '']);
|
||
vkFirmWithPhones(3);
|
||
|
||
Http::fake();
|
||
|
||
SyncVkAudienceJob::dispatchSync();
|
||
|
||
Http::assertNothingSent();
|
||
expect(SalesAdAudienceState::current()->vk_status)->toBe('no_access');
|
||
});
|
||
|
||
it('пока номеров меньше двух тысяч, не делает ни одного обращения к ВК', function () {
|
||
// 🔴 Порог 2000 — требование самого ВК к загрузке списка программой. Теперь
|
||
// берётся из строки площадки (vk.min_phones), дефолт 2000. Обращаться раньше
|
||
// бессмысленно и опасно: ВК считает попытки.
|
||
config(['services.vk_ads.token' => 'test-token']);
|
||
vkFirmWithPhones(3);
|
||
|
||
Http::fake();
|
||
|
||
SyncVkAudienceJob::dispatchSync();
|
||
|
||
Http::assertNothingSent();
|
||
|
||
$state = SalesAdAudienceState::current();
|
||
|
||
expect($state->vk_status)->toBe('waiting_volume')
|
||
->and($state->vk_last_error)->toBeNull();
|
||
});
|
||
|
||
it('фирмы, помеченные только Яндексом, в счёт для ВК не идут', function () {
|
||
config(['services.vk_ads.token' => 'test-token']);
|
||
|
||
$yandexOnly = SalesAdAudienceFirm::create([
|
||
'firm_name' => 'Только Яндекс', 'warmup_started_at' => now(), 'ch_yandex' => true,
|
||
]);
|
||
SalesAdAudiencePhone::create([
|
||
'phone' => '79990000001', 'firm_id' => $yandexOnly->id,
|
||
'expires_at' => now()->addDays(30), 'state' => 'active',
|
||
]);
|
||
|
||
Http::fake();
|
||
|
||
SyncVkAudienceJob::dispatchSync();
|
||
|
||
Http::assertNothingSent();
|
||
expect(SalesAdAudienceState::current()->vk_status)->toBe('waiting_volume');
|
||
});
|
||
|
||
it('состав ВК считается по срокам ВК, независимо от Яндекса', function () {
|
||
// 🔴 Смысл раздельных сроков: состав ВК считается ПОФИРМЕННО по срокам ВК, а
|
||
// не по сводному phone.state и не по срокам Яндекса. Фирма, погасшая по ВК,
|
||
// в состав ВК не входит — даже если по срокам Яндекса была бы ещё жива.
|
||
//
|
||
// 🪤 VkAudienceClient::replaceList — заглушка: программного доступа к ВК ещё
|
||
// нет, метод бросает исключение с ЧИСЛОМ готовых к отправке номеров в тексте
|
||
// (см. сам клиент). Джоб ловит его и кладёт в state.vk_last_error. Http сюда
|
||
// не задействован (в отличие от Яндекса), поэтому наблюдаем состав через это
|
||
// число: 1 = в состав попала только живая по ВК фирма; 2 = решение сломано и
|
||
// погасшая по ВК тоже просочилась. Порог опускаем до 1, чтобы джоб дошёл до
|
||
// попытки отправки и число стало наблюдаемым.
|
||
config(['services.vk_ads.token' => 'test-token']);
|
||
SalesAdAudiencePlatform::forCode('vk')->update(['enabled' => true, 'min_phones' => 1, 'warmup_days' => 3]);
|
||
SalesAdAudiencePlatform::forCode('yandex')->update(['warmup_days' => 30]);
|
||
|
||
// active по ВК (warmup 3, начат 1 день назад):
|
||
$live = SalesAdAudienceFirm::create(['firm_name' => 'Живая по ВК', 'warmup_started_at' => now()->subDay(), 'ch_vk' => true]);
|
||
SalesAdAudiencePhone::create(['phone' => '79990000011', 'firm_id' => $live->id, 'expires_at' => now()->addDays(30), 'state' => 'active']);
|
||
|
||
// погасла по ВК (warmup 3, начат 10 дней назад) — но по Яндексу (warmup 30) была бы жива:
|
||
$dead = SalesAdAudienceFirm::create(['firm_name' => 'Погасла по ВК', 'warmup_started_at' => now()->subDays(10), 'ch_vk' => true]);
|
||
SalesAdAudiencePhone::create(['phone' => '79990000022', 'firm_id' => $dead->id, 'expires_at' => now()->addDays(30), 'state' => 'active']);
|
||
|
||
SyncVkAudienceJob::dispatchSync();
|
||
|
||
// В состав ушла только живая по ВК фирма → к отправке готов ровно 1 номер.
|
||
$error = (string) SalesAdAudienceState::current()->vk_last_error;
|
||
|
||
expect($error)->toContain('Номеров готово к отправке: 1')
|
||
->and($error)->not->toContain('Номеров готово к отправке: 2');
|
||
});
|