Commit Graph

2 Commits

Author SHA1 Message Date
Дмитрий 026e71a283 feat(смс-клиент): цена по накоплению за месяц, а не по объёму одного заказа
Этап 3 «Время и цена», Task 6. Строки листа 3.8, 3.9, 3.10, 3.14.

Клиент, разбивший месяц на десять рассылок по сто номеров, платил по самой дорогой
ступени, хотя отправил тысячу. Авто-СМС и вовсе всегда считалась по самой дорогой:
в ступень уезжало число сегментов одного сообщения. Теперь ступень берётся для
«сколько отправлено в этом месяце плюс объём самого заказа», и спрашивают об этом
все три места сразу — предпросмотр, создание рассылки и авто-СМС на новый лид.

Счётчик живёт в одном месте (ClientSmsVolumeCounter): его зовёт цена, а в Task 7 позовёт
экран. Отдельного накопительного счётчика в базе не завожу намеренно — счётчик,
разъехавшийся с журналом, опаснее лишнего запроса; журнал правдив, потому что деньги
списываются той же записью.

Считаем в СМС, а не в сообщениях (В-108). Длинное письмо — это два СМС, и платит клиент
за два; считая строки журнала, мы держали бы его на дорогой ступени дольше обещанного, а
цифра на экране «в этом месяце отправлено N СМС» разошлась бы со списанными деньгами.
Ради этого в журнале появилась колонка segments (схема v9.14) и частичный индекс под
единственный запрос счётчика. Пусто у старых записей = одно СМС (В-109).

Граница месяца — по Москве, а не по Гринвичу (В-110): 31 июля 21:30 UTC это уже 1 августа
в Москве. Не путать с окном 10–20 — там время местное у получателя, здесь московское у
клиента.

Цена по-прежнему фиксируется в момент создания и джобом не пересчитывается (3.14).

🔴 Мина, найденная самопроверкой (В-114): авто-СМС считала бы объём месяца ВНЕ изоляции по
клиенту. В запросе экрана контекст ставит middleware, а в очереди — никто, и на бою
счётчик вернул бы честный ноль: клиента молча посчитали бы по самой дорогой ступени, без
единой ошибки в журнале. Тестами не ловится — на стенде изоляция не применяется. Счёт
переехал внутрь tenant-транзакции, как и деньги в том же джобе.

🧹 Убран прежний estimateRub (В-113): он считал смету по ступени для объёма одного заказа,
без накопленного, и больше не звался. Оставленный «на всякий случай» второй расчёт цены —
это место, которое однажды позовут, и цифры разъедутся.

Прогоны: СМС 229/229 (пачками по 3–4 файла — целиком локальная база уже не тянет, В-112),
приём лидов 17/17, фронт 1663 зелёных, phpstan 0 своих, pint чисто.

Вырезанием проверено четырежды: вернул старый расчёт в контроллер — покраснел тест через
настоящий запрос экрана; убрал запись числа СМС в журнал — покраснел тест отправки;
засчитал песочные — счётчик дал 12 вместо 1; перенёс границу месяца на Гринвич — покраснел
тест границы.

Живьём на локальной базе (20:17 МСК, песочница, ДаДата заглушена): предпросмотр до
накопления 9.00 ₽, после 5 000 отправленных — 8.00 ₽; песочная рассылка ушла, в журнале
«СМС=1», счётчик месяца остался нулём; у прежней рассылки цена так и осталась 9.00 ₽, а
новый заказ уже шёл бы по 8.00 ₽. Стенд возвращён как был. Реальное списание по
накопленной ступени доказано тестом, а не живьём: в песочнице деньги не двигаются (В-81).

🪤 Урок В-111: джоб авто-СМС глотает любой сбой и молча выходит — «ноль без причины» в
тестах надо смотреть в журнале сервера, там лежала точная строка про мою описку.
2026-07-28 20:28:00 +03:00
Дмитрий 3ec722416c feat(смс-клиент): сервисы — цена по ступеням, аудитория, отбор получателей
ClientSmsPricing (ступень по объёму получатели×сегменты, наценка не
применяется), ClientSmsAudienceBuilder (сделки/база/список, tenant-scope
SET LOCAL + явный where, нормализация телефона), ClientSmsRecipientSelector
(стоп-лист→дубль→маршрут), ClientSmsPlan DTO.

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