Commit Graph

5 Commits

Author SHA1 Message Date
Дмитрий b6c837e28a chore(смс-клиент): приёмка Этапа 4 — формат по всей области модуля, а не по свежим файлам
Task 8, приёмка Этапа 4. Кода она не приносит по замыслу: все двенадцать строк
листа (4.1–4.8, 4.11–4.14) закрыты ДВУМЯ доказательствами каждая — тест И живой
прогон, — экранные строки отдельно посмотрены глазами в браузере. Итоговая
таблица Этапа 4 заполнена в приёмочном листе (лист в git не лежит).

В git уходит ровно одна правка — порядок импортов в шести файлах тестов (В-192).
Причина, по которой она вообще нашлась: три этапа подряд `pint` гонялся ТОЛЬКО
по свежим файлам, а по всей области модуля не гонялся ни разу. Заодно выяснено,
что жалобы `line_ending` чинить не надо — это виндовые переводы строк рабочей
копии, в git их нет вовсе (pint «исправил», git не увидел ни одного изменения).
Перегнаны тесты этих шести файлов: 41/41 зелено.

🔴 ДЕНЕЖНАЯ МИНА СВЕРХ ПОСТРОЕННОГО (В-190). Права спросил у самой базы матрицей
по всем таблицам модуля — и увидел, что у рабочей роли crm_app_user есть право
писать в таблицу заморозок, а на её счётчик номеров ad_wallet_holds_id_seq права
нет. Живой прогон парно: без права заморозка денег под боевой ролью ПАДАЕТ («нет
доступа к последовательности»), с правом идёт. Заморозка делается при КАЖДОМ
заказе рассылки, при заказе имени и при запуске рекламной кампании Яндекса.
Промах был мой: в В-142 я привёл стенд к эталону двумя точечными командами по
именам из плана вместо `ON ALL SEQUENCES`, как делает сам db/02_grants.sql.
Памятка выката переписана: вместо списка имён счётчиков — запрос, который САМ
находит все счётчики без права. На бою проверить (косвенно там всё в порядке —
рекламные кампании запускаются тем же кодом, но довод косвенный).

🔴 И ПЕРВЫЙ ЗАХОД ПРИЁМКИ ПРОШЁЛ «ЗЕЛЕНО», НЕ ДОКАЗАВ НИ ОДНОЙ ДЕНЕЖНОЙ СТРОКИ
(В-191): песочница гасит и возврат заморозки при срыве, и ночного работника
целиком. Датчик на будущее — не сдвинулась ни одна копейка, значит прогон не
доказал ничего, даже когда всё зелено.

Ещё три промаха своих же приборов: В-193 и В-194 (прогон не доходил до
состояния — сторож законно даёт попытке дожать её срок, а имя без отметки
согласования кнопка законно не включает: правда была в коде), В-195 (браузерный
замер читал таблицу рассылок вместо базы и «доказал» поломку, которой нет —
класс В-121, соврал прибор).

Живьём под боевой ролью crm_app_user: сторож зависших тремя заходами (пометка →
выдержка срока → срыв, заморозка 17.00 → 0.00); имя за долг вернулось и списало
ровно 100 ₽, отключённое владельцем осталось выключенным при 5 000 ₽; кнопка
включения отказала числами, без пометки клиента 404, с пометкой списала 600 ₽;
номера руками легли пятью видами записи; продолжение довело 5 из 5 за 42.50 ₽.
Под служебной ролью — чистка снимка тройкой (без права падение, без политики
srv_bypass «успешный ноль», с обоими удалено 3).

Прогоны: ClientSms 320/320 (13 пачек, все с первой попытки), приём лидов 17/17,
фронт 1700 + 3 пропущенных (одна чужая давняя ошибка, В-57), phpstan ровно 2
чужие давние, vue-tsc ровно 5 чужих давних в 5 файлах (git blame: от 25.07),
pint чисто.

Стенд сверен со снимком «до» поле за полем и совпал. Намеренное изменение одно:
рабочей роли выданы права на ВСЕ счётчики схемы, как в эталоне db/02_grants.sql.

🟡 Открытый вопрос владельцу — В-182: номера в журнале сообщений живут без срока,
обязательство «90 дней» закрывает только снимок получателей.

Ветка НЕ влита в main и НЕ выкачена. Порядок выката: миграции →
db/03_service_bypass_policies.sql → подсчёт политик srv_bypass (+8) → права →
контрольный запуск чистки снимка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:05:08 +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
Дмитрий 1d8723f285 feat(смс-клиент): Калининград и Камчатка получают СМС каждый в своё утро
Этап 3 «Время и цена», Task 2. Строки листа 3.1, 3.2 и первая половина 3.3.

У каждой строки снимка получателей появились две вещи: часовой пояс человека и момент,
раньше которого сообщение отдавать нельзя. Считается это ОДИН раз, при создании рассылки:
снимок сильнее всего (В-39), а на 20 000 номерах пересчёт на каждом витке джоба был бы
20 000 лишних расчётов.

Три состояния строки, и их важно не путать:
  — пояс известен, ждать нечего  → отдаём прямо сейчас;
  — пояс известен, время не пришло → ждёт своего утра;
  — пояса нет                     → ждёт уточнения региона и НЕ уходит вовсе (В-85).

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

Рассылка, у которой часть номеров ещё ждёт, получает статус «ждёт утра» вместо «готово»,
и заморозка денег с неё не снимается — смета считалась на всех, оставшимся деньги ещё
понадобятся. Счётчик отправленного при этом обновляется: он считается из журнала, то есть
всегда правда, и человеку нужно видеть «отправлено 340 из 900» сразу, а не завтра (В-94).

Регион сделки читается из subject_code, а НЕ из region_code: последний в бою не пишет никто,
а в dev там демо-значения, и мы бы считали половину страны Тюменью — молча (В-82).

Прогоны: СМС 191/191 (было 186, 5 новых тестов), приём лидов 17/17, phpstan 0, pint чисто.
Вырезанием проверено дважды: убрал проверку «пояс известен» — покраснел тест про номер без
региона; убрал проверку «время пришло» — покраснели три теста про окно. Живьём на локальной
базе: две сделки (Москва и Камчатка) плюс пять демо-сделок без региона → ушёл один москвич,
Камчатке проставлено ожидание до 22:00 UTC (10 утра её времени), пятеро ждут региона,
рассылка висит «ждёт утра». Стенд возвращён как был.

В-93: у 19 старых тестов покраснение было закономерным — у их номеров нет региона. Боевой код
не ослаблял: фикстурам проставил регион и зафиксировал время прогона, иначе тесты зависели бы
от часа запуска. ⚠️ Ветку нельзя выкатывать между этой задачей и Task 5: пока ДаДата не начнёт
давать регион всем номерам, рассылка по своей базе и по списку руками отправит ноль.

Запись схемы v9.13.
2026-07-28 15:46:53 +03:00
Дмитрий 5b7d6e1bab fix(смс-клиент): «не определён оператор» вместо неправды «номер не из МТС»
Строка приёмочного листа 2.3. Раньше номер с НЕИЗВЕСТНЫМ оператором и номер
чужого оператора давали одну причину, и клиент читал про свой номер неправду:
«не из МТС» — хотя чей он, мы не знаем. Теперь причины две, и обе правдивы.

Так приходит большинство таких номеров: вписанные руками — без оператора
всегда, номера «своей базы» — до того, как ДаДата его проставит.

Причина живёт в четырёх местах, а не в трёх, как считал план: отборщик,
читатель снимка (журнал рассылки) и ДВА словаря подписей на экране. Счётчик в
контроллере складывает любые причины — править нечего.

Два капкана, оба доказаны вырезанием, а не рассуждением:

Порядок проверок. Отсеивать номер сразу, как только оператор не распознан (так
велел план), нельзя: универсальный канал берёт и номер без оператора, и такие
номера перестали бы уходить — рассылка молча уменьшилась бы при зелёных тестах.
Сперва спрашиваем маршрутизатор, причину называем только после отказа.
Поведение не изменилось ни на волос — изменилась надпись.

Деление. Словарь операторов отдаёт пустоту и когда оператора нет, и когда имя
есть, но словарь его не знает («Тинькофф Мобайл»). Делим по сырому значению,
иначе получилось бы новое враньё в другую сторону.

Тесты: +5 (три причины врозь, сторож универсального канала, журнал рассылки —
чтобы предпросмотр и журнал не расходились в словах). ClientSms 165/165, приём
лидов 17/17, фронт 1656 зелёных, phpstan 0, pint и eslint чисто.

Живой прогон: одна сводка сразу показывает и правду, и контроль — «Уйдёт 1 СМС
— 9.00 ₽ · Не уйдёт: не определён оператор — не знаем, куда слать — 1 · номер
не из МТС (пока шлём только по МТС) — 1». В журнале рассылки та же правда.

Экран пока НЕ подсказывает, что оператор ещё выясняется — отдельная работа,
записана в «Чего эта работа НЕ делает» п.16.
2026-07-28 10:58:45 +03:00
Дмитрий c5943ebb4e feat(смс-клиент): снимок получателей — считаем смету и шлём по одному списку
Аудитория собиралась дважды: в контроллере для сметы и заново в джобе для
отправки. Между двумя сборками приходят новые лиды, и «посчитали 900,
отправили 917» было физически возможно.

Теперь контроллер, посчитав смету, кладёт получателей в
client_sms_campaign_phones (две новые колонки: operator, skip_reason), а джоб
читает этот снимок пачками по 500 и ничего не пересобирает.

Решение владельца В-39: после запуска список не пересматривается ничем,
включая стоп-листы — смета и факт сходятся копейка в копейку. Человек,
внесённый в «Не писать этим» уже после запуска, эту рассылку получит;
следующую — нет. Следствие названо и владельцем принято.

Попутно убран квадрат: проверка «номер уже отправлен» шла через in_array по
массиву — на 20 000 номеров это 400 млн сравнений.

Строки приёмочного листа 2.1 и 2.2.

Проверено: ClientSms 144/144, приём лидов 17/17, phpstan по своим файлам 0,
вырезание записи снимка красит все 4 новых теста, живой прогон — контакт,
добавленный между созданием и отправкой, СМС не получил.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 05:37:16 +03:00