Files
portal/app/tests/Feature/ClientSms/SenderPerOperatorTest.php
T
Дмитрий 3512479e32
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat(смс): своё имя есть, но у этой сети не согласовано — номер не уходит вовсе
Решение владельца 05.08.2026. Прежний вариант (подписать нашим «Liderra.ru»)
подменял бренд, за который клиент заплатил, причём незаметно: журнал сообщений
имя отправителя не хранит вовсе. Так уже работает отдел продаж
(SmsRecipientSelector → SKIP_NO_SENDER), и надпись на клиентском экране
(«нет имени отправителя для оператора») давно лежала готовая — её просто никогда
не показывали.

Два случая разведены НАМЕРЕННО и закреплены отдельными тестами:
  • своего имени нет вовсе (или приостановлено) — шлём от имени Лидерры, как
    и раньше. Смешай их — и рассылка встала бы у ВСЕХ разом: на бою своих имён
    заведено НОЛЬ;
  • своё имя есть, но у этой сети не согласовано — номер не уходит.

Отсев в ОТБОРЩИКЕ, а не при отправке: деньги не замораживаются, и причина видна
клиенту в предпросмотре — до оплаты.

🪤 Полный прогон поймал мою ошибку, которую выборочные тесты не видели: имя
спрашивалось ПО КАЖДОМУ номеру — 20 000 запросов в базу, отбор 20 000 получателей
вырос с 20 до 29 секунд и упёрся в сторожа скорости. Добавлена памятка на тенанта
(та же защита, что у AllowedSmsOperators, который прямо об этом предупреждает).
После лечения — 1,47 с.

Два теста часовой давности переписаны: они закрепляли ПРЕЖНЕЕ решение и после
смены решения стали неправдой. Теперь проверяют и «в канал не ушло ничего», и
«деньги целы».

Узкая щель записана в коде: если админ сузит список операторов у имени ПОСЛЕ
создания рассылки, такой номер уже лежит в снимке и уйдёт подписанным нашим именем.
Случай редкий — пересогласование переводит имя в «на рассмотрении», а это снимает
блокировку.

Проверено: 4621 тест, упал один чужой (ExampleTest — в рабочей папке не собран
фронтенд). Статанализ 0. Стиль чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:21:01 +03:00

128 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ClientSmsSender;
use App\Models\Tenant;
use App\Services\ClientSms\ClientSmsSenderResolver;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* Имя отправителя согласовывает ОПЕРАТОР, а не канал: через универсальный
* СМС-центр можно слать в любую сеть, и спрашивать надо у той сети, чей номер.
*
* До 05.08.2026 клиентская рассылка несла ОДНО имя всем, а умолчанием служило
* `services.sms.mts.naming` — то есть имя, согласованное с МТС, подставлялось
* Теле2 и любому будущему каналу. Живой отказ `invalid_source_address` от Теле2
* 04.08 — ровно этот механизм.
*
* Пустая строка на выходе — НЕ «нет имени», а «своего имени нет, подпишись
* собственным именем канала»: каждый канал знает своё согласованное написание
* из настроек (MtsSmsProvider: `senderName !== '' ? senderName : $this->naming`).
*/
uses(DatabaseTransactions::class);
function makeSenderFor(?array $operators, string $status = ClientSmsSender::STATUS_ACTIVE, string $name = 'mybrand.ru'): Tenant
{
$tenant = Tenant::factory()->create();
ClientSmsSender::create([
'tenant_id' => $tenant->id,
'name' => $name,
'name_type' => ClientSmsSender::TYPE_WEBSITE,
'status' => $status,
'operators' => $operators,
'monthly_fee_rub' => '0.00',
'consent_at' => now(),
]);
return $tenant;
}
it('своего имени нет вовсе — отдаёт пустую строку, чтобы канал подписался своим', function () {
$tenant = Tenant::factory()->create();
expect(app(ClientSmsSenderResolver::class)->forOperator($tenant->id, 'tele2'))->toBe('');
});
it('имя согласовано у этого оператора — отдаёт его', function () {
$tenant = makeSenderFor(['mts', 'tele2']);
expect(app(ClientSmsSenderResolver::class)->forOperator($tenant->id, 'tele2'))->toBe('mybrand.ru');
});
it('имя согласовано у ДРУГОГО оператора — своим именем не подписывает', function () {
$tenant = makeSenderFor(['mts']);
// Ровно тот случай, что дал живой отказ Теле2 04.08.2026.
expect(app(ClientSmsSenderResolver::class)->forOperator($tenant->id, 'tele2'))->toBe('');
});
it('список операторов не заполнен — считаем, что имя годится всем', function () {
// Старые записи заведены до появления разбора по операторам: отнять у них
// имя молча — значит поменять поведение задним числом.
$tenant = makeSenderFor(null);
expect(app(ClientSmsSenderResolver::class)->forOperator($tenant->id, 'megafon'))->toBe('mybrand.ru');
});
it('имя ещё на согласовании — не подписывает им', function () {
$tenant = makeSenderFor(['tele2'], ClientSmsSender::STATUS_PENDING);
expect(app(ClientSmsSenderResolver::class)->forOperator($tenant->id, 'tele2'))->toBe('');
});
it('имя приостановлено за долг — не подписывает им', function () {
$tenant = makeSenderFor(['tele2'], ClientSmsSender::STATUS_SUSPENDED);
expect(app(ClientSmsSenderResolver::class)->forOperator($tenant->id, 'tele2'))->toBe('');
});
/**
* Решение владельца 05.08.2026: если у клиента СВОЁ имя есть, но у этой сети оно
* не согласовано — номер не уходит вовсе. Молча подписать чужую рассылку нашим
* «Liderra.ru» нельзя: клиент платил за свой бренд и об этой подмене даже не
* узнал бы (журнал сообщений имя отправителя не хранит).
*
* Так уже работает отдел продаж (SmsRecipientSelector → SKIP_NO_SENDER), и надпись
* на клиентском экране («нет имени отправителя для оператора») давно готова.
*/
it('своего имени нет вовсе — НЕ блокируем: шлём от имени Лидерры, как и раньше', function () {
$tenant = Tenant::factory()->create();
expect(app(ClientSmsSenderResolver::class)->blocksOperator($tenant->id, 'tele2'))->toBeFalse();
});
it('имя согласовано у этого оператора — не блокируем', function () {
$tenant = makeSenderFor(['mts', 'tele2']);
expect(app(ClientSmsSenderResolver::class)->blocksOperator($tenant->id, 'tele2'))->toBeFalse();
});
it('имя есть, но у ЭТОГО оператора не согласовано — блокируем', function () {
$tenant = makeSenderFor(['mts']);
expect(app(ClientSmsSenderResolver::class)->blocksOperator($tenant->id, 'tele2'))->toBeTrue();
});
it('список операторов не заполнен — не блокируем', function () {
$tenant = makeSenderFor(null);
expect(app(ClientSmsSenderResolver::class)->blocksOperator($tenant->id, 'megafon'))->toBeFalse();
});
it('имя приостановлено — не блокируем: это как если бы своего имени не было', function () {
// Иначе долг за имя останавливал бы рассылку целиком, а не возвращал её к
// имени Лидерры. Останавливать — отдельное решение, здесь его не принимаем.
$tenant = makeSenderFor(['mts'], ClientSmsSender::STATUS_SUSPENDED);
expect(app(ClientSmsSenderResolver::class)->blocksOperator($tenant->id, 'tele2'))->toBeFalse();
});
it('чужое имя соседа не подставляет', function () {
makeSenderFor(['tele2'], ClientSmsSender::STATUS_ACTIVE, 'sosed.ru');
$mine = Tenant::factory()->create();
expect(app(ClientSmsSenderResolver::class)->forOperator($mine->id, 'tele2'))->toBe('');
});