Найдено при чтении кода перед Task 11, вычищено по указанию владельца
«не оставляй хвостов» (журнал В-183).
Что было. Ветка `ClientSmsAudienceBuilder::fromManual` читала снимок
получателей и отсеивала строки по `expires_at`. При этом:
· колонку `expires_at` не заполняет НИКТО — ни контроллер, ни джобы, ни
обогащение ДаДатой; на стенде она пуста во всех строках;
· саму ветку не зовёт НИКТО: номера, вписанные руками, контроллер берёт
прямо из запроса и разбирает единственным домом
(`normalizeManualPhones`, строка листа 4.8).
То есть код обещал «у номера в списке есть срок годности» — поведение,
которого в портале нет. Опасность не теоретическая: я сам, начиная Task 11,
чуть не стал считать возраст снимка по этой колонке.
Что стало. Ветка убрана, на её месте ГРОМКИЙ отказ с объяснением, а не
пустой массив: пустая аудитория читалась бы как «рассылка честно ушла на
ноль номеров» — тот самый молчаливый сбой (В-121). Тест переписан на новое
поведение, мёртвый импорт убран.
🔴 Саму колонку `expires_at` НЕ удалял, и это осознанно: удаление
необратимо, а что лежит в ней на БОЕВОЙ базе, проверить нельзя — она по
умолчанию только для чтения и только с разрешения владельца. Оставлено
названным хвостом в приёмочном листе (п.30 «Чего эта работа НЕ делает»):
удаление — отдельная миграция со своим доказательством.
Проверено: ClientSms 315/315 (12 пачек, все с первой попытки), приём лидов
17/17, phpstan ровно 2 чужие давние, pint чисто. Фронт не трогался.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Этап 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.
ClientSmsPricing (ступень по объёму получатели×сегменты, наценка не
применяется), ClientSmsAudienceBuilder (сделки/база/список, tenant-scope
SET LOCAL + явный where, нормализация телефона), ClientSmsRecipientSelector
(стоп-лист→дубль→маршрут), ClientSmsPlan DTO.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>