0]), ]); return new ClientSmsRecipientSelector($router, new OperatorNormalizer, new AllowedSmsOperators(new OperatorNormalizer)); } it('своё имя есть, но у ЭТОЙ сети не согласовано — номер не уходит', function () { $tenant = Tenant::factory()->create(); ClientSmsSender::create([ 'tenant_id' => $tenant->id, 'name' => 'mybrand.ru', 'name_type' => ClientSmsSender::TYPE_WEBSITE, 'status' => ClientSmsSender::STATUS_ACTIVE, 'operators' => ['tele2'], // а номер ниже — МТС 'monthly_fee_rub' => '0.00', 'consent_at' => now(), ]); $plan = client_sms_selector()->build( [['phone' => '79990000041', 'operator' => 'mts']], $tenant->id, ); expect($plan->sendable)->toBeEmpty() ->and($plan->skipped)->toBe(['79990000041' => ClientSmsMessage::SKIP_NO_SENDER]); }); it('своего имени нет вовсе — номер уходит от имени Лидерры, как и раньше', function () { // Страховка от главной беды этой правки: на бою своих имён заведено НОЛЬ. // Заблокируй мы и этот случай — рассылка встала бы у всех разом. $tenant = Tenant::factory()->create(); $plan = client_sms_selector()->build( [['phone' => '79990000042', 'operator' => 'mts']], $tenant->id, ); expect($plan->skipped)->toBeEmpty() ->and($plan->sendable)->toHaveCount(1); }); 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); });