Files
portal/app/tests/Feature/ClientSms/AdminGlobalOptoutTest.php
T
Дмитрий a39694e11b feat(смс-клиент): общий стоп-лист портала — номер закрыт у всех клиентов сразу
Этап 1 «Нельзя обжечься», задачи 1–3 приёмочного листа
(docs/superpowers/2026-07-27-PRIEMKA-client-sms-fixes.md, строки 1.4, 1.4а, 1.5).

- новая таблица sms_global_optouts (SaaS-уровень, RLS намеренно нет: номер
  закрывается у всех тенантов сразу — защита договора с МТС при жалобе);
- ClientSmsRecipientSelector отсеивает такой номер ПЕРВЫМ, раньше тенантского
  стоп-листа: наше обязательство перед оператором сильнее настроек клиента;
- клиенту причина видна словами — «номер закрыт администрацией», а не молчаливое
  «не отправлено» (решение владельца, вопрос В-2 листа);
- админ-адреса /api/admin/sms/global-optouts: внести (номер в любом виде),
  список, убрать; непонятый номер отклоняется внятно;
- запись v9.00 в db/CHANGELOG_schema.md.

Проверено: ClientSms 122/122, приём лидов 17/17, phpstan по своим файлам 0,
gitleaks чисто. Защита доказана вырезанием — без отсева падают ровно 3 теста.

Живого прогона строк 1.4/1.5 ещё нет: админ-экран идёт задачей 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 17:17:20 +03:00

72 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SmsGlobalOptout;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* Строки листа 1.4 и 1.5: владелец вносит номер в общий стоп-лист портала
* (с комментарием, зачем), видит список внесённых и может номер убрать.
* Закрытый номер перестаёт получать СМС у ВСЕХ клиентов сразу — проверка
* самого отсева живёт в RecipientSelectorTest.
*/
uses(DatabaseTransactions::class);
test('владелец вносит номер в общий стоп-лист с комментарием', function () {
$this->postJson('/api/admin/sms/global-optouts', [
'phone' => '+7 900 111-22-33',
'reason' => 'жалоба через МТС',
])->assertStatus(201);
$row = SmsGlobalOptout::find('79001112233');
expect($row)->not->toBeNull()
->and($row->reason)->toBe('жалоба через МТС');
});
test('номер принимается в любом виде — 8, +7, со скобками', function () {
foreach (['8 (900) 111-22-44', '+79001112255', '9001112266'] as $raw) {
$this->postJson('/api/admin/sms/global-optouts', ['phone' => $raw])->assertStatus(201);
}
expect(SmsGlobalOptout::whereIn('phone', ['79001112244', '79001112255', '79001112266'])->count())
->toBe(3);
});
test('непонятый номер отклоняется внятно, а не молча', function () {
$this->postJson('/api/admin/sms/global-optouts', ['phone' => 'абырвалг'])
->assertStatus(422)
->assertJsonPath('message', 'Не понял номер телефона');
expect(SmsGlobalOptout::count())->toBe(0);
});
test('повторное внесение того же номера не ломается и не плодит записей', function () {
$this->postJson('/api/admin/sms/global-optouts', ['phone' => '79001112233', 'reason' => 'первая'])
->assertStatus(201);
$this->postJson('/api/admin/sms/global-optouts', ['phone' => '79001112233', 'reason' => 'вторая'])
->assertStatus(201);
expect(SmsGlobalOptout::where('phone', '79001112233')->count())->toBe(1)
->and(SmsGlobalOptout::find('79001112233')->reason)->toBe('первая');
});
test('владелец видит список внесённых номеров', function () {
SmsGlobalOptout::create(['phone' => '79001112233', 'reason' => 'жалоба оператору']);
SmsGlobalOptout::create(['phone' => '79001112244']);
$r = $this->getJson('/api/admin/sms/global-optouts')->assertStatus(200);
expect($r->json('total'))->toBe(2)
->and($r->json('items'))->toHaveCount(2);
});
test('владелец убирает номер из общего стоп-листа', function () {
SmsGlobalOptout::create(['phone' => '79001112233']);
$this->deleteJson('/api/admin/sms/global-optouts/79001112233')->assertStatus(200);
expect(SmsGlobalOptout::find('79001112233'))->toBeNull();
});