Files
portal/app/tests/Feature/Sales/VkAudienceSyncTest.php
T

150 lines
8.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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');
});