Files
portal/app/tests/Feature/ClientSms/RecipientSelectorTest.php
T
Дмитрий 3ec722416c feat(смс-клиент): сервисы — цена по ступеням, аудитория, отбор получателей
ClientSmsPricing (ступень по объёму получатели×сегменты, наценка не
применяется), ClientSmsAudienceBuilder (сделки/база/список, tenant-scope
SET LOCAL + явный where, нормализация телефона), ClientSmsRecipientSelector
(стоп-лист→дубль→маршрут), ClientSmsPlan DTO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 20:59:35 +03:00

127 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ClientSmsMessage;
use App\Models\ClientSmsOptout;
use App\Models\Tenant;
use App\Services\ClientSms\ClientSmsRecipientSelector;
use App\Services\Sms\OperatorNormalizer;
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('неизвестный/пустой оператор тоже помечается skipped_no_route', function () {
$tenant = Tenant::factory()->create();
$planNull = client_sms_selector()->build(
[['phone' => '79990000005', 'operator' => null]],
$tenant->id,
);
$planGarbage = client_sms_selector()->build(
[['phone' => '79990000006', 'operator' => 'garbage']],
$tenant->id,
);
expect($planNull->skipped)->toBe(['79990000005' => ClientSmsMessage::SKIP_NO_ROUTE])
->and($planGarbage->skipped)->toBe(['79990000006' => ClientSmsMessage::SKIP_NO_ROUTE]);
});
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 () {
$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);
});