Строка приёмочного листа 2.8. Больше 365 дней задать нельзя, и человек читает
почему: «Больше 365 дней нельзя — возьмите срок покороче».
Правило max:365 стоит в общей проверке полей — значит и на предпросмотре, и на
создании рассылки, мимо экрана его не обойти. Заодно человеческий текст у нижней
границы: стандартный называл поле программистским именем audience_days.
На экране проверка настоящая, а не только атрибут max у поля: напечатать 400
руками браузер спокойно даёт, и атрибут при этом молчит. При запрещённом сроке
экран показывает сообщение, запрос на сервер не уходит и смета гасится — иначе
кнопка «Отправить» осталась бы живой со старым числом получателей.
Тесты: 2 новых серверных (отказ с человеческим текстом на обеих дорогах +
контрольная граница ровно 365) и 2 на экране. ClientSms 158/158, приём лидов
17/17, фронт 1654 зелёных. Проверено вырезанием четырежды.
Живой прогон: 30 дней — «Уйдёт 5 СМС — 45.00 ₽»; напечатал 400 — сообщение,
смета исчезла, кнопка заперлась, на сервер не ушло ни одного запроса; вернул
365 — смета снова на месте. Прямыми запросами мимо экрана сервер отказывает тем
же текстом и рассылку не создаёт.
Доработки экрана «Моя база» клиентской СМС-рассылки по просьбе владельца:
- Кривые номера больше не выбрасываются молча: uploadContacts/uploadContactsFile
возвращают rejected + rejected_samples, экран показывает «Добавлено: N,
не распознали: M (например: …) — проверьте формат».
- Таблица базы — только «Номер» и «Оператор» (колонку «Имя» убрал).
- Загрузка базы Excel-файлом: сервис ClientSmsPhoneFileReader (phpspreadsheet)
читает первую колонку, пропускает заголовок, номера-как-числа не уходят в
научную нотацию; endpoint POST /api/sms/contacts/file; на экране — выбор файла
и кнопка «Загрузить файл».
- «Скачать пример файла»: ClientSmsBaseExampleWriter + GET /api/sms/contacts/example
— готовый .xlsx с колонкой «Телефон» и образцами номеров.
- Оператор через ДаДату: EnrichClientSmsContactsOperatorJob обогащает добавленные
номера (DaDataPhoneClient.provider) в фоне, под tenant-контекстом (RLS), с
защитой дневного бюджета (DaDataBudgetGuard); сбой ДаДаты не роняет прогон,
оператор остаётся пустым; уже известного оператора не перезапрашиваем.
Тесты: бэкенд ClientSms 130/130, фронт СМС-спеки 63/63 — зелёные; pint чист;
портал пересобран. Синтетические номера 7999… в тестах.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корректность/деньги:
- джоб рассылки: списание ПОШТУЧНО и атомарно с записью в журнал, ключ идемпотентности на номер, факт из БД — нет недосписания при падении посреди рассылки и повторе
- валидация: срок обязателен при source=deals, иначе выборка по всей истории сделок
- «Отправить» гаснет при нехватке свободного баланса; index отдаёт баланс/заморозку
- помесячный джоб имени: устойчив к пустым настройкам, сигнал в лог при автоотключении за долг
- цена одного авто-СМС + подтверждение при включении авто-рассылки
- загрузка контактов сообщает число нераспознанных номеров
Понятность для клиента:
- «СМС» вместо «сегментов», «пробный режим» вместо «песочницы», человеческие причины пропуска, шаги 1-2-3, выгода своего имени, факт вместо оценки в таблице, имя и текст в подтверждении
Тесты: бэкенд ClientSms 83/83, фронт СМС 50/50, приём лидов 10/10; pint/stan чисто.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>