5b7d6e1bab
Строка приёмочного листа 2.3. Раньше номер с НЕИЗВЕСТНЫМ оператором и номер чужого оператора давали одну причину, и клиент читал про свой номер неправду: «не из МТС» — хотя чей он, мы не знаем. Теперь причины две, и обе правдивы. Так приходит большинство таких номеров: вписанные руками — без оператора всегда, номера «своей базы» — до того, как ДаДата его проставит. Причина живёт в четырёх местах, а не в трёх, как считал план: отборщик, читатель снимка (журнал рассылки) и ДВА словаря подписей на экране. Счётчик в контроллере складывает любые причины — править нечего. Два капкана, оба доказаны вырезанием, а не рассуждением: Порядок проверок. Отсеивать номер сразу, как только оператор не распознан (так велел план), нельзя: универсальный канал берёт и номер без оператора, и такие номера перестали бы уходить — рассылка молча уменьшилась бы при зелёных тестах. Сперва спрашиваем маршрутизатор, причину называем только после отказа. Поведение не изменилось ни на волос — изменилась надпись. Деление. Словарь операторов отдаёт пустоту и когда оператора нет, и когда имя есть, но словарь его не знает («Тинькофф Мобайл»). Делим по сырому значению, иначе получилось бы новое враньё в другую сторону. Тесты: +5 (три причины врозь, сторож универсального канала, журнал рассылки — чтобы предпросмотр и журнал не расходились в словах). ClientSms 165/165, приём лидов 17/17, фронт 1656 зелёных, phpstan 0, pint и eslint чисто. Живой прогон: одна сводка сразу показывает и правду, и контроль — «Уйдёт 1 СМС — 9.00 ₽ · Не уйдёт: не определён оператор — не знаем, куда слать — 1 · номер не из МТС (пока шлём только по МТС) — 1». В журнале рассылки та же правда. Экран пока НЕ подсказывает, что оператор ещё выясняется — отдельная работа, записана в «Чего эта работа НЕ делает» п.16.
221 lines
9.2 KiB
PHP
221 lines
9.2 KiB
PHP
<?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\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);
|
||
}
|
||
|
||
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()` отдаёт пустую строку И когда оператора нет, И когда
|
||
// имя есть, но нашего словаря он не знает («Тинькофф Мобайл», МВНО). Делить
|
||
// по этой пустоте нельзя: про названного оператора «не определён» — новое
|
||
// враньё, только в другую сторону. Делим по СЫРОМУ значению.
|
||
$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_NO_ROUTE]);
|
||
});
|
||
|
||
it('2.3 — сторож: при универсальном канале номер без оператора УХОДИТ, а не отсеивается', function () {
|
||
$tenant = Tenant::factory()->create();
|
||
|
||
// 🪤 В-67, сторож к порядку проверок. Универсальный канал (СМС-центр,
|
||
// `serves: ['*']`, в песочнице — заглушка) берёт любой номер, в том числе без
|
||
// оператора. Если отсеивать «оператор не распознан» ДО вопроса маршрутизатору
|
||
// — как предлагал план Task 7 — такие номера перестанут уходить, и рассылка
|
||
// молча уменьшится. Сначала спрашиваем маршрут, причину называем только когда
|
||
// он отказал.
|
||
$universal = new ClientSmsRecipientSelector(
|
||
new SmsRouter([new FakeSmsProvider]),
|
||
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);
|
||
});
|