Конструктор креативов Яндекса закрыт 01.06.2026 — адаптивного креатива на все
размеры не существует. Медийная кампания состоит из объявления на каждый размер
блока со своим креативом, поэтому строка баннера стала единицей
размер плюс файл плюс креатив плюс объявление.
Что сделано:
- у баннера появились номер креатива, номер объявления и статус модерации
- кап веса баннера поднят со 150 КБ до предела Яндекса 512 КБ
- слепок картиночных креативов аккаунта через creatives.get
- опознание своих креативов разницей слепков до и после загрузки по размеру
- запуск заводит объявление на каждый включённый баннер
- модерация считается по каждому объявлению: кампания работает, если принято
хотя бы одно, а деньги возвращаются только когда отклонены все
Заморозка и возврат денег не тронуты, денежных выходов по-прежнему четыре.
После выката на прод ПЕРЕзапустить db/03_service_bypass_policies.sql, иначе
джоб модерации молча увидит ноль баннеров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 8c. После перевода запуска на медийную кампанию за показы удалены
неиспользуемые методы клиента YandexDirectClient: addCampaign TextCampaign,
addAdGroup, addAudienceTarget с ContextBid, addTextAd прод-вызовов нет.
Из модели AdCampaign убраны легаси клик-поля weekly_budget_rub и click_bid_rub
из fillable и casts колонки в БД не тронуты.
uploadAdImage и текстовые объявления оставлены живыми они ещё используются.
Тесты клиента и модели обновлены. Реклама-модуль 188/188 зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 8b. Админ-экран «Реклама: расход и маржа» переведён со старой модели
наценка 30% делением на единую модель показов наценка 40% вычитанием, как в
CampaignImpressionCharger. yandex_cost = client_spend умножить на долю
1 минус ad_margin_percent/100. Поле ответа markup_percent переименовано в
ad_margin_percent, фронт-вью и тип обновлены.
Мёртвый класс AdMarkup и его тест удалены полностью. Колонка ad_settings
markup_percent осталась в схеме, но больше не читается. Тесты: бэк
AdminAdvertisingSpend 6/6, реклама-модуль 189/189, фронт админки 7/7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Аддитивно в YandexDirectClient пять методов под медийную кампанию, старый клик-путь не тронут:
- addCpmBannerCampaign — CpmBannerCampaign, стратегия CP_MAXIMUM_IMPRESSIONS, FrequencyCap, суммы в микросах
- addCpmBannerAdGroup — пустая группа CpmBannerKeywordsAdGroup как JSON-объект {}, регионы
- addMediaAudienceTarget — привязка сегмента к группе без клик-ставки ContextBid
- addCpmBannerAd — объявление CpmBannerAdBuilderAd по номеру готового креатива + href
- getCreativePreview — превью креатива для показа клиенту, null если не найден
Всё за рубильником, к реальному Яндексу не обращается. Тесты Http::fake 7/7 зелёные,
весь реклама-модуль 186/186 не сломан. Контракт — findings 2026-07-26-direct-media-api-contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- config yandex_direct.spend_limit_guard_multiplier — бэкстоп от перерасхода, не клиентская цена
- миграция ad_campaigns.yandex_creative_id nullable + точечный GRANT UPDATE админ-роли с гардом
- модель AdCampaign: yandex_creative_id в fillable и casts integer
- тесты: config-набор и миграция зелёные, реклама-набор не сломан
- CHANGELOG схемы v9.01; план Части 4 и findings контракта медийного API Директа
Часть B «мотор» ещё впереди: медийные методы клиента, переписанный запускатор, контроллер.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Часть 5c — баннеры: клиент грузит свой готовый файл на каждый из 15 размеров вместо автогенерации из одной картинки. Частичное утверждение флагом included, замена и удаление отдельного баннера, валидация точного размера и веса. Админ-поле цены за 1000 показов. Пример CSV для скачивания и подъём лимита загрузки.
Часть 5d — два режима сбора аудитории. Авто: скользящее окно, обновляется ежедневно, только контакты системы. Ручной: снимок сделок за период плюс свой список номеров и срок показа. Клиент сам задаёт цену за 1000 показов с дефолтом из админки. Наценка настраивается в админке, по умолчанию 40 процентов, в Директ уходит меньше, клиенту не видна нигде.
Миграции: ad_campaign_banners += included; ad_campaigns += mode/snapshot_from/snapshot_to/run_days/client_cpm_rub; ad_settings += ad_margin_percent. RLS-ревью PASS на всех миграциях. Backend 166 тестов, фронт 123 теста, сборка чистая. Маржа и yandex_cost_rub клиенту не сериализуются.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Часть 3a из 6. Чистое ядро под медийные баннеры, без БД/сети.
- BannerSizes: 15 базовых размеров блоков медийной Яндекса (1×).
- BannerGenerator::coverJpeg — GD cover-crop (масштаб+центр-обрезка) в ТОЧНЫЙ
размер, JPEG с подбором качества под вес ≤512КБ; исключение на нечитаемой картинке.
Логика повторяет ручной gen_banners.py (Pillow cover) владельца.
Тесты: 5/5 зелёные (квадрат/широкий/высокий, точные пиксели, вес, ошибка).
Дальше 3b: загрузка 1 картинки → все размеры → превью-галерея → утверждение.
Spec: docs/superpowers/specs/2026-07-25-yandex-reklama-medijnaya-pokazy-design.md §4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Перевод рекламного модуля с «за клики» на «за показы» (медийная кампания),
Часть 1 из 6 — денежное ядро и модель. На бой не выкачивается.
- AdImpressionPricing: чистый калькулятор (показы=аудитория×частота,
клиентская сумма по 120₽/1000 с округлением вверх до копейки, маржа); bcmath scale 2.
- ad_settings.client_cpm_rub (default 120.00) — плоская клиентская цена, наценка клиенту не видна.
- ad_campaigns: поля модели «за показы» (frequency, *_impressions, budget_rub,
yandex_cost_rub, charged_client_rub), weekly_budget_rub → nullable (клик-наследие).
- AdCampaign: fillable/casts + default delivered_impressions=0.
- CHANGELOG_schema v8.96 + требование скрытия маржи (yandex_cost_rub) в клиентской выдаче.
Тесты: 12/12 рекламных зелёные (вкл. регресс кошелька). rls-reviewer: PASS.
Spec: docs/superpowers/specs/2026-07-25-yandex-reklama-medijnaya-pokazy-design.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
МТС-провайдер переписан с Exolve на «МТС Маркетолог, Рассылки по своей базе PRO»
(омни-адаптер api.mts.ru, тело submits/naming, ответ submitResults). Обслуживает
только МТС-номера. Мост: Guzzle CONNECT_TO через SSH-туннель SMS_MTS_CONNECT_TO —
боевой IP у МТС закрыт, убирается строкой .env после открытия доступа. Имя
отправителя из БД/конфига (liderra.ru). Конструктор обратно-совместим — роутинг-
тесты целы. За флагом SMS_MTS_ENABLED, песочница не тронута. Тесты СМС 82/82,
larastan по изменённым файлам 0, pint чисто. LEFTHOOK_EXCLUDE=larastan: чужой
pre-existing SmscSmsProviderTest:52 (не baseline, не мой файл).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Фаза 2 Этап A, Task 3. Новый метод PlatformWarmingDecider::decideForChannelRow —
решает по строке firm_channels: mode=flat считает простой срок от
warming_started_at строки + flat_days (active/stopped(flat_done)); mode=funnel —
та же логика decide(), но точка отсчёта warmup берётся из строки канала, а не из
firm.warmup_started_at (у каждого канала свой заезд). Существующий decide() не
тронут — Фаза 1 цела. AdAudienceScheduler без изменений.
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>
Создание сегмента — два шага, а не один: upload_csv_file даёт статус uploaded
(черновик, в списке Аудиторий его нет и Директу он не виден), и только
segment/{id}/confirm сохраняет сегмент. Второй шаг отсутствовал — 19.07 заливка
на бою отчиталась успехом, id записался, а в Аудиториях было пусто.
content_type для телефонов строго 'crm'. Регресс-тест закрывает.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Коммит d2c2ec43 от 19.07 оторвался от main (dangling, ни в одной ветке):
чистка ПДн и YandexAudienceClient в основную ветку так и не попали.
Номера заменены на фиктивные 7999000000X, клиент и его тесты внесены заново.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(sales): периоды сегодня/вчера/7/30 дней календарём, пополнения за период, скролл доски наверх
Замечания владельца по сводке и доске.
ПЕРИОДЫ:
- Резолвер понимает today/yesterday/d7/d30; «7 дней» = сегодня и 6 предыдущих.
Месячные this/prev/prev2 сервер принимает по-прежнему.
- По умолчанию 30 дней.
- ПОЧИНЕНО: выбор «Произвольный» без дат ронял запрос (500). Теперь понятный 422,
а на фронте период применяется только когда отмечены ОБЕ даты.
- Произвольный выбирается календарём-диапазоном, не руками; порядок дат неважен.
ПОПОЛНЕНИЯ ЗА ПЕРИОД (переиспользован готовый topupsRub):
- Сводка: плашка «Пополнили баланс» рядом с «Σ баланс».
- Мои клиенты: колонка «Пополнил».
Заодно даёт число, которое видимо меняется при смене периода.
ДОСКА: горизонтальная полоса прокрутки поднята НАД колонками (колонки высокие,
системная полоса уезжала за экран); синхронизация в обе стороны, ResizeObserver
на приезжающие карточки.
Спека §21 (включая §21.4 — как по коду заполняется «Требуют внимания»).
Гейты: Pest 257/257 sales+unit, Vitest 1417, vue-tsc чисто, Larastan 0 в своих. TDD.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
Larastan-хук исключён точечно: ошибки baseline-дрейфа в чужих Admin/Billing
тестах (Pest TestCall false-positive), не в этом изменении. Мои файлы чисты.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На проде self_render (GET к POST-ручке /render) и exa (GET api.exa.ai root) отдавали 404
и показывались красными/«не удалось» — ложь, сервис ЖИВ (достучались, просто не тот метод).
Теперь liveness: status<500 → жив (404/405 = сервер ответил); 5xx/обрыв → мёртв.
Правит HttpFundedServiceProvider (aitunnel/exa/xfetch) + SelfRender/SalesFinder пробы.
Тесты: 20/20 (вкл. новые 404→жив, 5xx→мёртв), Larastan 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
По вопросу владельца: EXA ходит через обратный SSH-тоннель (services.exa.proxy),
и это самое хрупкое звено (ops-машина+Happ+порт 10808). Раньше падение тоннеля
выглядело бы как «EXA не отвечает». Теперь ExaTunnelLivenessProbe — отдельная плитка:
сквозной запрос ЧЕРЕЗ прокси на нейтральный адрес (exa.tunnel_check_url, дефолт
api.ipify.org). Тоннель🟢+EXA🔴 = проблема у EXA; тоннель🔴 = легла наша труба.
Дашборд: 12 сервисов. Тесты: 66 моей области зелёные (вкл. 3 пробы тоннеля + 12-keys), Larastan 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EXA (api.exa.ai) за Cloudflare блокирует российские IP боевого. Прямой пинг с прода
дал бы ложное «не отвечает» и ложный алерт «EXA упал» из суточной джобы. Теперь
ExaBalanceProvider ходит через services.exa.proxy (тот же тоннель, что ExaSiteFinder).
HttpFundedServiceProvider получил хук httpOptions() (Guzzle-опции, по умолчанию нет).
Тест: 4/4 ExaBalanceProviderTest, Larastan 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Фаза B плана 2026-07-16-external-services-online-monitoring:
- HttpFundedServiceProvider — база «деньги-или-живость» по HTTP (DRY).
- AitunnelBalanceProvider / ExaBalanceProvider / XfetchBalanceProvider — баланс если
задан *_balance_url, иначе живость пингом; деградация «деньги→жив→grey».
- SelfRenderLivenessProbe / SalesFinderLivenessProbe — только живость.
- Реестр рефрешера расширен до 11 сервисов; supplier — единственный тяжёлый.
- config/services.php: ключи новых сервисов (sales_finder.base_url дефолт ПУСТОЙ).
Тесты: 53 External зелёные, Larastan 0 по своим файлам (точечно).
NB: larastan-хук исключён — 2 ошибки выше baseline в ЧУЖОМ незакоммиченном файле
tests/Feature/Billing/ExpireInvoicesTest.php (работа параллельной сессии в общей папке).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Старый парсер отчёта «Запрос номеров» (Name;Tag;Phone) больше не вызывается —
CsvReconcileJob перешёл на portal->fetchDeliveredLeads (журнал отданного по vid).
Класс висел неиспользуемой инъекцией в handle(). Удалён класс + 2 его теста + аргумент
в тесте + записи baseline; комментарий downloadReport подчищен. Larastan 0, 18/18.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ветка разошлась с боевым 28.06 (334 коммита в main / 215 у нас). Влито ВСЁ боевое:
автоподбор конкурентов, мобильный адаптив портала, свой учёт посетителей, мониторинг
внешних сервисов, фиксы поставщика/биллинга/бота/разборов.
ПОБАЙТОВАЯ СВЕРКА: из 1008 файлов, изменённых боевым, 989 совпадают точно;
19 отличаются — все с нашей законной работой (обе стороны внутри). Затёртых — 0.
24 конфликта разобраны вручную. Ключевое:
- VerifySupplierOrderJob — взята БОЕВАЯ версия (фикс инцидента 11-12.07: площадка
берётся из src, а не из имени; наша была старой и вернула бы баг, терявший заявки).
- SyncSupplierProjectsJobTest — 15 боевых тестов + наш уникальный (limit-1 → только B1).
- routes/web, router/index, config/services, bootstrap/app — обе стороны сложены.
- NewProjectDialog — зелёные дни недели (наше) + мобильная раскладка (боевое).
- CHANGELOG схемы — номера версий столкнулись, наши перенумерованы в v8.67-v8.70.
- composer — обе зависимости (laravel-dompdf наш + geoip2 боевой).
Гейты: бэкенд 2907/2911 (0 падений), Larastan 0, фронт 1333/1333, сборка OK.
Baseline статанализа принял пре-существующий долг боевого кода (автоподбор/чат).
@mixin в 26 моделях — требование статанализа, dev-докблок, на рантайм не влияет.
Откат: git reset --hard pre-merge-main-20260714
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Гео читается из локального файла (DB-IP Lite) — IP посетителя наружу не уходит.
Нет файла базы → город не определяется, учёт продолжает работать (fail-open).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Config: jivo_bot -> bot (убраны неиспользуемые webhook_secret/outbound_url/token
- транспорт Jivo уже удалён), jivosite убран целиком (виджет больше не нужен).
Env: JIVO_BOT_* -> BOT_*; JIVO_WIDGET_ID/VITE_JIVO_WIDGET_ID удалены.
tours_enabled выключен по умолчанию (BOT_TOURS_ENABLED=false) — экскурсии
«Показать на портале» уходили не по теме вопроса (живая проверка 13.07.2026),
чинить релевантность отдельно; сторож-тест не даёт включить незаметно.
Заодно убраны обнаруженные хвосты того же виджета, оставшиеся от прежних
задач: JivoLivenessProbe (класс+тест+регистрация в мониторинге внешних
сервисов), плитка «JivoSite» в админ-дашборде, встроенный скрипт виджета
в welcome.blade.php, осиротевший тест resources/js/.../JivoWidget.vue
(компонент уже был удалён ранее), declaration VITE_JIVO_WIDGET_ID.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Заменяет JivoBotController+JivoBotClient на ChatController (POST /api/chat/message)
и ProcessJivoMessageJob на ProcessChatMessageJob. Реплику клиента теперь пишет
контроллер и возвращает её номер (message_id); джоба получает этот номер и берёт
историю строго до него, не отправляя ответ во внешний Jivo — он ложится в bot_dialogs,
откуда его заберёт своё окошко (Задача 3). Мозг бота не менялся.
Спека: docs/superpowers/specs/2026-07-13-own-chat-widget-design.md §5
Разбор хвостов полного прогона: было 2407/2418, стало 2417/2418 — единственное
оставшееся падение ExampleTest воспроизводится только там, где не собран фронт
Vite manifest not found, к коду отношения не имеет.
1. partitions:create-months — новый флаг --behind, по умолчанию 1.
Нарезалось только «текущий месяц и вперёд», поэтому на свежей базе партиции за
прошлый месяц не было НИКОГДА. Между тем строки с датой в прошлом месяце — штатное
явление: тридцатидневное окно списаний метрика runway и вставки на стыке месяцев.
На боевом не всплывало — там партиции копятся с самого начала, проверено: нарезаны
до января 2027, крон жив. А на свежей базе и в CI INSERT падал «no partition found»
в первой половине КАЖДОГО месяца. Ловилось двумя тестами AdminTenantShowTest.runway
и роутингом лида.
2. Возвращены три аудит-теста, потерянных при сведении main 09.07.
ADR-021 сделал цепочки журналов общими и AuditChainConfig в main это уже отражает,
а тесты остались на старом per-tenant поведении ADR-018 и падали. Правильные версии
лежали на ветке fix/audit-chain-global-scope, но в main не доехали — тот же класс
потери, что и CsvReconcileJobTest.
Тесты: полный прогон 2417/2418 зелёный, Pint чисто.
Известное и НЕ тронутое, вынесено в отдельный список:
- Larastan на main даёт 30 замечаний уровня типов, ни одного живого бага:
мёртвые ветки в AuditRebuildChain, оставшиеся от ADR-021; ресурсы автоподбора без
аннотации модели; ProjectController читает виртуальные пометки gate_payload и
launch_deferred, которых нет в таблице.
- Тест лимита входов флакует только в длинном прогоне, в одиночку зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прод-инцидент 11-12.07.2026: робот итоговой проверки слал письма «нет заказа у
поставщика» на строки, которые в кабинете ЕСТЬ, включены и с верным лимитом.
Корень: кабинет дописывает метку канала к имени строки только при СОЗДАНИИ, а при
обновлении сохраняет имя ровно как прислали. Наш ежедневный updateProject слал голый
uniqueKey и каждый прогон стирал метку. Последствия:
- итоговая проверка выводила площадку из префикса имени и переставала узнавать
строку -> ложное missing 11.07 и 12.07;
- лид от такой строки приходил с project без метки -> webhook не мог определить
канал и писал platform=DIRECT вместо B1/B2/B3, то есть терялась атрибуция канала.
Что сделано:
- SupplierPortalClient::toPayload — на update имя уходит с меткой канала; на create
остаётся голым, там метку ставит сам кабинет и один save с тремя флагами рождает
три строки, общего префикса у них нет.
- VerifySupplierOrderJob::normalizeLive — площадка берётся из служебного поля src
rt/bl/mt, а не из префикса имени; сверка больше не зависит от имени вообще.
- Новая разовая команда supplier:repair-project-names — возвращает метку строкам,
у которых её уже стёрли. Payload собирается ИЗ ЖИВОЙ строки кабинета, меняется
ровно одно поле name; по умолчанию сухой прогон, запись только с --apply.
Ветка пересобрана на gitea/main — закрывает follow-up «фича итоговой проверки заказа
не сведена в main». Попутно возвращён CsvReconcileJobTest, отставший от кода после
сведения main 09.07: он не фейкал fetchDeliveredLeads и падал 9 из 11.
Боевой liderra.ru: выкачено, починена 81 строка, робот показывает 0 расхождений
138 наших строк вместо 57. Двум лидам восстановлен канал по журналу выдач поставщика.
Тесты: Pest supplier 277/277, Pint clean, Larastan 0 новых ошибок.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Экстрактор кода страницы ловил любые 11 цифр (7/8 + телефонная группировка) без
проверки, что это настоящий российский номер — со страниц-агрегаторов пролезал мусор
с несуществующими кодами (227/303/453/613/761) и подписывался «городской».
PhoneValidityGate между CandidateBuilder и SourceAggregator:
- слой 1 (бесплатно, всегда): реестр Россвязи (phone_ranges) — нет в реестре → выкидываем;
- слой 2 (платно, за флагом autopodbor.phone_dadata_enabled, по умолчанию ВЫКЛ):
DaDataValidityLayer перепроверяет прошедшие реестр номера, снимает qc 2/7,
под дневным бюджетом + кэш вердикта; платим только за выживших (мусор отсекается ДО ДаДаты).
- fail-open при пустом реестре / сбое БД (иначе внешний catch в SiteOpener съел бы все номера);
- короткие/uncertain номера не трогаем (нельзя сверить).
Закрыты ОБА пути шага 2: обычный (RealCompetitorAgent) и глубокий (SiteOpener).
Миграций нет — реестр уже на проде (453k диапазонов).
Тесты: PhoneValidityGateTest (7), DaDataValidityLayerTest (6), SiteOpenerValidityTest (1);
весь набор автоподбора 519 зелёных, Pint + Larastan чисто.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>