Files
portal/app/tests/Feature/ClientSms/RecipientSelectorTest.php
T

229 lines
10 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\ClientSmsMessage;
use App\Models\ClientSmsOptout;
use App\Models\SmsGlobalOptout;
use App\Models\Tenant;
use App\Services\ClientSms\AllowedSmsOperators;
use App\Services\ClientSms\ClientSmsRecipientSelector;
use App\Services\Sms\OperatorNormalizer;
use App\Services\Sms\Providers\FakeSmsProvider;
use App\Services\Sms\Providers\MtsSmsProvider;
use App\Services\Sms\SmsRouter;
use Illuminate\Foundation\Testing\RefreshDatabase;
/**
* Отбор получателей клиентской СМС-рассылки (Task 5). Получатели уже несут
* оператора (его определил ClientSmsAudienceBuilder) — здесь только стоп-лист,
* дубли внутри рассылки и маршрут. Никакого отсева по каналу отправителя —
* Фаза 1 всегда шлёт от имени liderra.ru.
*
* Роутер собран руками (не app()-синглтон): в песочнице singleton — заглушка,
* которая маршрутизирует всё подряд, и тест ничего бы не проверил.
*/
uses(RefreshDatabase::class);
function client_sms_selector(): ClientSmsRecipientSelector
{
$router = new SmsRouter([
new MtsSmsProvider(token: 'test', serves: ['mts'], priceKopecks: ['*' => 0]),
]);
return new ClientSmsRecipientSelector($router, new OperatorNormalizer, new AllowedSmsOperators(new OperatorNormalizer));
}
it('стоп-лист побеждает даже подходящий маршрут', function () {
$tenant = Tenant::factory()->create();
ClientSmsOptout::create([
'tenant_id' => $tenant->id,
'phone' => '79990000001',
]);
$plan = client_sms_selector()->build(
[['phone' => '79990000001', 'operator' => 'mts']],
$tenant->id,
);
expect($plan->sendable)->toBeEmpty()
->and($plan->skipped)->toBe(['79990000001' => ClientSmsMessage::SKIP_OPTOUT]);
});
it('номер МТС уходит в отправку через МТС-провайдера', function () {
$tenant = Tenant::factory()->create();
$plan = client_sms_selector()->build(
[['phone' => '79990000002', 'operator' => 'mts']],
$tenant->id,
);
expect($plan->skipped)->toBeEmpty()
->and($plan->sendable)->toHaveCount(1)
->and($plan->sendable[0]['phone'])->toBe('79990000002')
->and($plan->sendable[0]['operator'])->toBe('mts')
->and($plan->sendable[0]['provider'])->not->toBeNull()
->and($plan->sendable[0]['provider']->key())->toBe('mts');
});
it('оператор без маршрута помечается skipped_no_route', function () {
$tenant = Tenant::factory()->create();
$plan = client_sms_selector()->build(
[['phone' => '79990000003', 'operator' => 'beeline']],
$tenant->id,
);
expect($plan->sendable)->toBeEmpty()
->and($plan->skipped)->toBe(['79990000003' => ClientSmsMessage::SKIP_NO_ROUTE]);
});
it('2.3 — номер без оператора получает причину «не определён оператор», а не «не из МТС»', function () {
$tenant = Tenant::factory()->create();
// Оператора в данных НЕТ: так приходят номера, вписанные руками, и номера
// «своей базы», которым ДаДата ещё не проставила оператора. Раньше им
// писалось «не из МТС» — а мы не знаем, чей это номер (строка листа 2.3).
$planNull = client_sms_selector()->build(
[['phone' => '79990000005', 'operator' => null]],
$tenant->id,
);
$planEmpty = client_sms_selector()->build(
[['phone' => '79990000007', 'operator' => ' ']],
$tenant->id,
);
expect($planNull->skipped)->toBe(['79990000005' => ClientSmsMessage::SKIP_UNKNOWN_OPERATOR])
->and($planEmpty->skipped)->toBe(['79990000007' => ClientSmsMessage::SKIP_UNKNOWN_OPERATOR]);
});
it('2.3 — оператор назван, но канала для него нет: это «не из МТС», а не «не определён»', function () {
$tenant = Tenant::factory()->create();
// 🪤 В-68: `canonical()` отдаёт пустую строку И когда оператора нет, И когда
// имя есть, но нашего словаря он не знает («Тинькофф Мобайл», МВНО). Делить
// по этой пустоте нельзя: про названного оператора «не определён» — новое
// враньё, только в другую сторону. Делим по СЫРОМУ значению.
//
// 🔴 Изменение 30.07 (журнал В-155, строка листа 4.14): мелкий оператор
// теперь выбывает РАНЬШЕ — на списке разрешённых, а не на маршруте, и
// причина у него своя. «Нет маршрута» осталось для оператора ИЗ четвёрки,
// которого сейчас некому везти: здесь роутер собран без универсального
// канала, и билайновский номер до него не доезжает.
$planKnown = client_sms_selector()->build(
[['phone' => '79990000006', 'operator' => 'beeline']],
$tenant->id,
);
$planMvno = client_sms_selector()->build(
[['phone' => '79990000008', 'operator' => 'Тинькофф Мобайл']],
$tenant->id,
);
expect($planKnown->skipped)->toBe(['79990000006' => ClientSmsMessage::SKIP_NO_ROUTE])
->and($planMvno->skipped)->toBe(['79990000008' => ClientSmsMessage::SKIP_OPERATOR_DENIED]);
});
it('2.3 — сторож: при универсальном канале номер без оператора УХОДИТ, а не отсеивается', function () {
$tenant = Tenant::factory()->create();
// 🪤 В-67, сторож к порядку проверок. Универсальный канал (СМС-центр,
// `serves: ['*']`, в песочнице — заглушка) берёт любой номер, в том числе без
// оператора. Если отсеивать «оператор не распознан» ДО вопроса маршрутизатору
// — как предлагал план Task 7 — такие номера перестанут уходить, и рассылка
// молча уменьшится. Сначала спрашиваем маршрут, причину называем только когда
// он отказал.
$universal = new ClientSmsRecipientSelector(
new SmsRouter([new FakeSmsProvider]),
new OperatorNormalizer,
new AllowedSmsOperators(new OperatorNormalizer),
);
$plan = $universal->build(
[['phone' => '79990000009', 'operator' => null]],
$tenant->id,
);
expect($plan->skipped)->toBeEmpty()
->and($plan->sendable)->toHaveCount(1)
->and($plan->sendable[0]['provider']->key())->toBe('fake');
});
it('дубль внутри одной рассылки считается один раз', function () {
$tenant = Tenant::factory()->create();
$plan = client_sms_selector()->build(
[
['phone' => '79990000004', 'operator' => 'mts'],
['phone' => '79990000004', 'operator' => 'mts'],
],
$tenant->id,
);
expect($plan->sendable)->toHaveCount(1)
->and($plan->skipped)->toBe(['79990000004' => ClientSmsMessage::SKIP_DUPLICATE]);
});
it('номер из общего стоп-листа портала не получает рассылку', function () {
$tenant = Tenant::factory()->create();
SmsGlobalOptout::create(['phone' => '79990000010', 'reason' => 'жалоба оператору']);
$plan = client_sms_selector()->build(
[['phone' => '79990000010', 'operator' => 'mts']],
$tenant->id,
);
expect($plan->sendable)->toBeEmpty()
->and($plan->skipped)->toBe(['79990000010' => ClientSmsMessage::SKIP_GLOBAL_OPTOUT]);
});
it('общий стоп-лист закрывает номер у ВСЕХ тенантов сразу', function () {
$tenantA = Tenant::factory()->create();
$tenantB = Tenant::factory()->create();
SmsGlobalOptout::create(['phone' => '79990000011']);
foreach ([$tenantA->id, $tenantB->id] as $tenantId) {
$plan = client_sms_selector()->build(
[['phone' => '79990000011', 'operator' => 'mts']],
$tenantId,
);
expect($plan->sendable)->toBeEmpty()
->and($plan->skipped)->toBe(['79990000011' => ClientSmsMessage::SKIP_GLOBAL_OPTOUT]);
}
});
it('общий стоп-лист проверяется РАНЬШЕ тенантского — причина видна именно общая', function () {
$tenant = Tenant::factory()->create();
SmsGlobalOptout::create(['phone' => '79990000012']);
ClientSmsOptout::create(['tenant_id' => $tenant->id, 'phone' => '79990000012']);
$plan = client_sms_selector()->build(
[['phone' => '79990000012', 'operator' => 'mts']],
$tenant->id,
);
expect($plan->skipped)->toBe(['79990000012' => ClientSmsMessage::SKIP_GLOBAL_OPTOUT]);
});
it('стоп-лист чужого тенанта не глушит тот же номер у своего', function () {
$tenantA = Tenant::factory()->create();
$tenantB = Tenant::factory()->create();
ClientSmsOptout::create([
'tenant_id' => $tenantB->id,
'phone' => '79990000007',
]);
$plan = client_sms_selector()->build(
[['phone' => '79990000007', 'operator' => 'mts']],
$tenantA->id,
);
expect($plan->skipped)->toBeEmpty()
->and($plan->sendable)->toHaveCount(1);
});