Строка листа 4.6 состояла из двух половин, и вторая («десятки тысяч
проходят и не падают») не выполнялась вовсе: каждый номер писался
отдельным запросом — 20 000 номеров стоили 25 278 запросов и 41 секунду.
Теперь пишем пачками по 1000 одним upsert: 23 запроса и 3.5 секунды.
Оплаченный ДаДатой оператор при повторной загрузке не стирается, дубли
внутри одной загрузки не роняют её, база не задваивается.
Потолок: колонка client_sms_settings.max_upload_phones (миграция
2026_08_01_100800, схема v9.20), по умолчанию 50 000, правится владельцем
в админке в границах 1 000…100 000. Сверх потолка загрузка отклоняется
целиком — частично загруженная база хуже незагруженной — и человек видит
оба числа. Экран говорит потолок ДО загрузки, числом с сервера.
Потолок спрашивается ПЕРЕД построчной проверкой номеров: иначе отказ на
50 001 номере занимал 20 секунд (замерено живым прогоном), а при верхней
границе запрос успел бы умереть по сроку жизни.
Ответ GET /api/sms/contacts стал объектом {items, max_upload_phones};
мёртвое поле contacts из ответа загрузки убрано.
Проверено: 14 серверных тестов, 2 фронтовых, 8 вырезов (каждый покраснел
там, где вырезан), живой прогон под боевой ролью crm_app_user и в браузере.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Строка листа 4.3, Этап 4 Task 2.
Срок, после которого сторож считает рассылку зависшей, переехал из кода в
админку «СМС» — рядом с границами окна отправки. Поле подписано человеческими
словами и объясняет последствие: портал сперва попробует рассылку дожать, потом
остановит и вернёт клиенту замороженные деньги, а ту, что честно ждёт утра
получателей, не тронет.
Границы 5…1440 минут — не вкусовщина. Ниже пяти сторож срывал бы рассылки,
которые просто идут медленно: проверка средств и запись в журнал на каждый номер
занимают время. Выше суток чужие деньги висели бы замороженными дольше, чем
клиент вообще помнит про эту рассылку. Отказы написаны словами, а не кодами.
Поле НЕобязательное — как и границы окна: этот адрес зовут и те, кому нужны
только настройки имени отправителя, и ломать им запросы права не имеем.
(План велел сделать поле обязательным — это была ошибка плана, журнал В-139.)
Проверено вырезанием, два выреза, оба вернуты:
— убрал границы на сервере → покраснели оба теста про границы;
— убрал отправку срока с экрана → покраснели три фронтовых, включая новый.
Живой прогон в браузере: выставил 120 → сохранилось → пережило перезагрузку
страницы; ввёл 2 минуты → отказ «Слишком мало: рассылка может идти медленно,
и сторож срывал бы живые. Ставьте хотя бы 5 минут», в базу не легло. Вернул 60.
Прогоны: затронутые тесты 32/32, фронт целиком 1678 + 3 намеренно пропущенных
(было 1676, мои +2; одна ошибка — та же чужая давняя), vue-tsc ровно 8 чужих
давних в 6 файлах, pint чисто.
ClientSmsController (index/preview/store/show, база контактов, шаблоны;
tenant по $request->user()->tenant_id + явный where; 409 при нехватке денег,
freeze при создании). AdminSmsTariffController (правка ступеней и настроек,
зона saas-admin/admin-db, crm_admin_user). Маршруты /api/sms и /api/admin/sms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>