Фаза 2 Этап B, Task B1 (+ план этапа B). AdAudienceIntake::ingest больше не
запускает прогрев: грузит фирму во все 4 канала (yandex/vk/mts/sms) строками
firm_channels status='loaded' (firstOrCreate — не понижает уже греющийся канал).
ch_* на заезде не пишутся (источник членства — строки firm_channels, resolveChannels
удалён). Повторный заезд обновляет только снимок-поля и НЕ сбрасывает
warmup_started_at/ready_at/stopped_at/stop_reason (заезд ≠ рестарт прогрева).
Фирма без warming-строк ни в одну заливку не идёт ⇒ автостарта нет по построению.
Запуск прогрева — кнопкой «Греть» на портале (этап C). Тесты intake мигрированы
на новую семантику. Bump baseline (+6 postJson-ложняков).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Начальник отдела продаж отмечает фирмы прогрева галочками, пишет текст,
видит цену ДО отправки и журнал после. Отправки СМС в проекте не было
вообще — ни у СМС-центра (только баланс и HLR), ни у МТС (только файл).
Что сделано:
- разъём провайдера SmsProvider: новый оператор подключается одним файлом
- заглушка FakeSmsProvider — модуль работает и проверяется ДО согласования
имени отправителя у операторов (это недели), иначе разработку не закончить
- маршрутизация по оператору: билайновский номер уходит через Билайн за
4,75 ₽, прочие через МТС — без ручного выбора канала
- стоп-лист: кто отписался, тому не шлём никогда, проверка перед списанием
- отбор получателей с шестью причинами пропуска, все ДО траты денег
- списание скопировано с AutopodborChargeService; пока клиента нет
(tenant_id пуст) с баланса не берём — платим оператору напрямую
- оператор номера доезжает из «Поиска клиентов» в прогрев (был известен
и оплачен ДаДате, но терялся при передаче)
Мультиклиентность в костях: колонка tenant_id во всех четырёх таблицах
СМС с первого дня, NULL = «Лидерра сама». Клиент добавляется строкой,
а не переделкой модуля.
Найдено и закрыто при исполнении:
- замок от двойного списания стоял не на том соединении: кампания на
pgsql_supplier, деньги на pgsql, lockForUpdate по кампании отпускался
сразу. На бою два запуска списали бы дважды, обрыв — оставил бы пометку
«оплачено» при неушедших деньгах. Источник правды перенесён в
balance_transactions под замок по тенанту. Доказано тестом: старый код
списывал 700 вместо 850
- приём в портал требовал phones строкой по regex — словарь с оператором
получал 422, в базу не доезжало ничего. Тесты были зелёные, потому что
звали сервис МИМО контроллера. Проверка теперь принимает оба формата,
тест идёт через HTTP
- телефоны директоров в contacts остаются строками (договор
SalesProspectController), словари — только в верхнем phones
Заодно вылечена мигающая поломка 48 тестов доставки лидов: помощник
createRoutingSnapshotFromProject клал снимок на сегодня, а LeadRouter
после 21:00 МСК ищет завтрашний (вечерний переворот заливки) — вечерние
прогоны падали, дневные проходили. Помощник теперь зеркалит активную дату
роутера в любой час. Регрессия SnapshotHelperTimeOfDayTest замораживает
22:00 МСК и пинит инвариант. Боевой LeadRouter не тронут.
Тесты: 84 бэкенд + фронт по экрану + 397 поисковика, весь набор 3226
зелёный, статанализ чист. Все защиты проверены вырезанием.
План: docs/superpowers/plans/2026-07-20-sms-progrev-modul.md
Спека: docs/superpowers/specs/2026-07-20-sms-progrev-modul-design.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'channels' убрали из $fillable в Task 2 плана tri-ploshadki-i-mts, но
AdAudienceIntake остался класть его в атрибуты — Laravel тихо
выбрасывал ключ при mass assignment. Итог: каждая новая фирма из
«Поиска клиентов» получала ch_yandex=false/ch_vk=false/ch_mts=false —
не грелась нигде, без единой ошибки в логах.
Контракт принимает оба формата: новые булевы поля ch_yandex/ch_vk/
ch_mts (приоритет) и старую строку channels ('yandex'|'vk'|'both')
для совместимости с ещё не переехавшей Python-службой. Если не
пришло ни то, ни другое — ch_yandex=true (фирма отправлена в прогрев
осознанно, разумное умолчание — основная площадка).
Регресс-тест ловил баг до фикса: 3/3 новых теста падали с
"Failed asserting that false is true" на ch_yandex/ch_vk.
Коммит d2c2ec43 от 19.07 оторвался от main (dangling, ни в одной ветке):
чистка ПДн и YandexAudienceClient в основную ветку так и не попали.
Номера заменены на фиктивные 7999000000X, клиент и его тесты внесены заново.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(sales): приём номеров в рекламную аудиторию
Сервис-канал POST /api/sales/integration/ad-audience (тот же X-Sales-Token,
что и «Отдать менеджеру»). Новый номер добавляем, известный — продлеваем срок
из настройки и просим ночной джоб дослать (synced_at=NULL, removed_at=NULL).
Формат Яндекса строгий: 11 цифр с 7, без плюса.
Task 3 плана 2026-07-19-reklamnaya-auditoriya-kabinet-nachalnika.
Моделям дописаны @property-докблоки (конвенция проекта, как у SalesProspect) —
без них Larastan не видит полей.
Тесты: 6 в tests/Feature/Sales/AdAudienceIntakeTest.php, вся Sales-пачка 264 зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@