Commit Graph

7 Commits

Author SHA1 Message Date
Дмитрий 5240cfbdd9 style: пробел перед @mixin в шапках моделей — так их пишет форматтер
Правка целиком в комментариях: 37 файлов, ни строки кода.
Её дописал сам pre-commit форматтер во время предыдущей записи и оставил
неподтверждённой. Записываю, чтобы папка ветки не оставалась грязной.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 19:57:50 +03:00
Дмитрий b6a15c5bbf merge: подтянул общую ветку в клиентские СМС — 227 записей отставания закрыты
Ветка шла отдельно почти неделю и отставала на 227 записей, отставание росло
каждый день. Направление сведения — общая В ветку: перевод main владелец
отклонил, значит вливать в него нечего.

Девять столкновений, каждое разобрано по существу.

Журнал схемы: столкнулись НЕ три номера, как ожидалось, а ВСЕ - обе ветки
независимо заняли v8.96-v9.25 и v9.32 разным содержимым. Обе стороны
настоящие, выбросить нельзя ни одну, поэтому перенумерована ветка, а не
общая: 31 запись уехала в свободный диапазон v9.33-v9.63. Содержание не
тронуто - доказано сверкой с исходной версией через git, посимвольно.
Соответствие старых номеров новым вписано в сам журнал, чтобы старые
документы ветки оставались читаемыми. Прежняя пометка про "запас v9.32"
заменена: запас не спас, v9.32 в общей ветке тоже был занят.

Сборка тестовой базы: взята версия общей ветки. Она позже и доказана
замером - двумя шагами вместо migrate:fresh, который спотыкался на
типе-призраке и оставлял схему неполной.

Список слов орфографии сведён объединением: 2106 наших + 2169 общих дали
2173, ни одно слово ни с одной стороны не потеряно - проверено сравнением.

Расписание работ, маршруты экранов и админский слой: обе стороны добавляли
своё в одно место, оставлены обе.

Витрина рекламных каналов: каждая ветка сделала настоящим СВОЙ канал -
ветка СМС свой, общая Телеграм. После сведения настоящих три, заглушки
исключают все три. Сторож витрины принят вырезанием: убрал СМС из списка
настоящих - покраснел, вернул - позеленел.

СТОЛКНОВЕНИЕ ИМЁН, созданное самим сведением. Оба набора тестов объявляли
глобального помощника pollCampaign - свой в СМС (один довод) и свой в
Телеграме (от двух до четырёх). Две функции с одним именем в одном языке
не живут: пока ветки шли врозь, этого не видел никто. Помощник СМС
переименован в pollSmsCampaign. Проверено, что других таких пар в PHP-тестах
нет ни одной.

Статанализ ветки доведён с 674 замечаний до НУЛЯ, уровень не понижен и в
baseline не заметено ничего.
  - 616 из 674 - ложный класс Pest, закрытый тремя узкими правилами; правила
    перенесены из рабочей ветки, где владелец их уже принял;
  - остальные 42 - свои, в новом коде ветки, и починены по существу:
    задвоенный ключ массива в трёх тестах (след копирования - комментарий
    оторвался от своей строки), врущие описания двух помощников (PHP сам
    делает из ключа-номера число), сужение типа возврата, прятавшее от
    анализатора свойства подставного отправителя, лишний знак вопроса и
    четыре бесполезных перенумерования списка.
  - Приёмка вырезанием: подложил несуществующий метод - анализатор назвал его
    поимённо и покраснел; убрал - ноль.

Шапки 59 моделей обновлены пересборкой подсказчика и ОСТАВЛЕНЫ намеренно
(правка только в комментариях, проверено): без них анализатор не связывает
модель с описанием и не знает, что дата - это дата, а не строка. Откатил их
сперва по привычке - получил 15 замечаний про даты, вернул - ноль.

Орфография: 15 файлов проверено, 0 замечаний (смотрел и на число
проверенных файлов, не только на число ошибок). Добавлены три слова из имён
миграций ветки.

Заодно: алиас ruflo-core в списке имён сторожа реестра - плагин описан в
реестре групповым именем, сторож видел только машинное. Мостится ТОЛЬКО имя;
🔴 содержательный долг остаётся - реестр до сих пор зовёт ruflo изолированным,
хотя его разморозили 28.07. Это чинить отдельно, через claude-md-management.

Проверено: три фронтовых сторожа рекламы 18/18, статанализ 0, разметка 0,
орфография 0, синтаксис PHP чист. Полный прогон тестов ветки - отдельным
шагом, он ещё ни разу не делался.

NB: в журнале схемы есть задвоенные номера v8.26 (пять раз) и v8.64 (два) -
это досталось по наследству из общей ветки, ровно столько же их там и было.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 19:50:41 +03:00
Дмитрий 1d292082ab feat(смс-клиент): потолок загрузки базы и пачечная запись вместо построчной
Строка листа 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>
2026-07-29 17:30:03 +03:00
Дмитрий 65e270583c feat(смс-клиент): шлём только абонентам МТС, Билайна, Мегафона и Теле2
Строка листа 4.14, решение владельца В-149 (вариант Б). Номер мелкого или
виртуального оператора в рассылку не берётся, и человек видит честную причину,
а не молчаливую пропажу.

Каналы отправки НЕ тронуты: МТС возит своих, остальных троих — универсальный
канал СМС-центра. Ограничиваем, КОГО берём, а не КЕМ везём.

Список — настройкой, а не в коде: client_sms_settings.allowed_operators
(схема v9.19), галочки «Кому шлём» в админке, пусто = четвёрка по умолчанию.
Правило живёт в одном месте — AllowedSmsOperators.

Решение принимается ДВАЖДЫ, и второй раз — единственная возможность: у сделок
и своей базы оператор известен в момент заказа, у номеров, вписанных руками,
его нет вовсе, и приговор выносится в момент ответа ДаДаты — в снимок ложится
уже канонический ключ, где «Тинькофф Мобайл» неотличим от «ещё не спрашивали».

Плата за имя не тронута (В-150): в коде два похожих списка операторов, и
связать их значило бы поднять плату всем клиентам с 2500 до 10 000 рублей.

Заодно починена давняя неправда на экране (В-154): «номер не из МТС (пока шлём
только по МТС)» — универсальный канал возит всех.

Доказательства: 8 новых тестов (в т.ч. сторож длины слага причины — колонка
24 знака), 6 вырезов, живой прогон с выключенной песочницей и пара на момент
ответа ДаДаты, живой прогон в браузере со снятием галочки «Билайн».
ClientSms 281/281, приём лидов 17/17, фронт 1685, phpstan 2 чужие давние,
vue-tsc 5 чужих давних, pint чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:25:37 +03:00
Дмитрий c0a8da37d2 feat(смс-клиент): зависшая рассылка сама срывается и возвращает замороженные деньги
Строки листа 4.1 и 4.2, Этап 4 Task 1.

Беда, от которой сторожим: работник очереди умирает посреди отправки
(перезапуск сервера, обрыв связи). Снятие заморозки денег стоит ПОСЛЕДНИМ
шагом джоба отправки — до него он в этом случае не доходит. Итог на бою:
рассылка вечно «идёт», деньги клиента заморожены навсегда, в журнале тишина.

Команда client-sms:watch-stuck, каждые 15 минут. Движение меряется временем
последней записи в журнале рассылки: sent_count для этого не годится, он
проставляется только в самом конце. Порядок — решение владельца В-132:
сперва ОДНА попытка дожать (это безопасно, джоб пропускает уже отправленные
номера по ключу на номер), и только если и после неё не сдвинулась — срываем,
размораживаем остаток, ставим причину «сторож». Итог считаем из журнала.

Честно ждущая утра рассылка не трогается вовсе (строка 4.2): отличаем по
состоянию — ждущая waiting_window, зависшая sending. Ждущих дожимает
client-sms:resume-waiting.

Миграция 2026_08_01_100500 — две колонки: срок «зависла» в настройках
(правит владелец, строка 4.3) и отметка попытки дожать у рассылки. Прав не
требуют, наследуют привилегии таблиц; повторный накат переживают. Схема v9.17.

Проверено вырезанием, три выреза, все вернуты:
— убрал условие про состояние → покраснел тест 4.2, ждущую положили в очередь;
— убрал ветку «сперва дожать» → покраснел тест «кладётся в очередь ещё раз»;
— убрал пометку клиента → живой прогон под боевой ролью показал молчаливый сбой.

Живой прогон ПОД БОЕВОЙ РОЛЬЮ crm_app_user, парно:
с пометкой клиента — рассылка сорвана, заморозка 17.00 → 0.00;
без пометки — осталась «идёт», 17.00 зависли,
и в ОБОИХ случаях команда сказала «Сорвано: 1» и вернула успех.

Прогоны: ClientSms 249/249 (было 241, +8; 11 пачек, все с первой попытки),
приём лидов 17/17, phpstan ровно 2 чужие давние, pint чисто. Фронт не трогался.
2026-07-29 09:44:42 +03:00
Дмитрий 86d974330b feat(смс-клиент): правило «с 10 до 20 по местному» живёт в одном месте, границы правит владелец
Этап 3 «Время и цена», Task 1. Закрыты строки листа Н.3 и 3.7.
Поведение рассылки ещё НЕ меняется — этим займётся Task 2. Сейчас заведено то,
на чём оно будет стоять, и заведено так, чтобы правило нельзя было размножить.

1. Справочник часовых поясов (RegionTimezoneMap). 89 субъектов РФ в том же порядке,
   что и справочник имён; сторож-тест сверяет составы, чтобы справочники не разъехались.
   Отдельный тест на ловушку: код субъекта у нас НЕ автомобильный — 77 это Тюменская
   область, а Москва 82. Неизвестный код и непонятная строка от ДаДаты дают «не знаю»,
   а не ноль: ноль означал бы Гринвич, то есть тихую подмену Камчатки Лондоном.

2. Единственный дом правила 10–20 (SmsQuietHours): можно ли отдавать сейчас, когда
   откроется окно, осмысленно ли такое окно. Границы читаются из настроек один раз
   на объект — на 20 000 номеров иначе был бы 20 000-й запрос к базе.

3. Границы окна в общих настройках: миграция добавляет две колонки со значениями 10 и 20.
   Защита от повторного запуска пошаговая — прерванная ручная подача SQL на бою не должна
   оставить вторую колонку несозданной (урок В-80). Прав не требует: колонки наследуют
   привилегии таблицы. Запись схемы v9.12.

4. Админка «СМС»: два поля «Отправляем с / по» и объяснение, что часы — по местному времени
   получателя и клиент их не настраивает. Окно наизнанку «с 20 до 10» это отправка всю ночь,
   поэтому сервер его не принимает и говорит человеку почему. Проверяется ПОЛУЧИВШЕЕСЯ окно,
   а не присланные поля: правка одной границы тоже могла его вывернуть — журнал В-91.

Прогоны: СМС 186/186 (было 171, 15 новых тестов), приём лидов 17/17, фронт 1658 зелёных
и 3 намеренно пропущенных, phpstan по своим файлам 0, pint чисто, проверка типов без новых ошибок.
Вырезанием проверено дважды: убрал проверку окна — покраснели два теста; убрал чтение границ
с сервера на экране — покраснел фронтовый тест. Живьём: окно 11–19 сохранилось и пережило
перезагрузку, «с 20 до 10» отклонено с человеческим текстом, вернул 10–20.

Попутный урок В-92: два моих же новых теста сперва зеленели по неверной причине — ругань
приходила за пропущенные поля платы за имя, а не за окно. Теперь тесты шлют полное письмо
и проверяют, за какое поле ругаются.
2026-07-28 15:20:13 +03:00
Дмитрий c661b99097 feat(смс-клиент): таблицы ядра рассылки + тарифы + RLS + модели
8 таблиц client_sms_* (6 tenant-RLS + 2 глобальных тарифы/настройки),
политики tenant_isolation по образцу ad_wallets, гранты crm_app_user /
crm_admin_user (гварды ролей). Модели ядра + сид ступеней и настроек.

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