# Lead Region Resolution — runbook раскатки на прод

> Фича: определение настоящего региона лида по телефону (DaData → реестр Россвязи →
> tag-fallback) + каскадная маршрутизация по региону. Код реализован и зелёный
> (Сессии 1-6, TDD). Этот runbook — порядок выкатки оператором на `liderra.ru`.
> Spec: `docs/superpowers/specs/2026-05-29-lead-region-resolution-design.md` v0.5.
> Plan: `docs/superpowers/plans/2026-05-29-lead-region-resolution.md`.

## Решение D1 (зафиксировано заказчиком 01.06.2026)

**Вариант В** — внутри каждой ступени каскада при >3 претендентах лид раздаётся
**взвешенным жребием по остатку дневного лимита**: шанс ∝ остатку, но у каждого
кандидата шанс > 0 (вес ≥ 1) — маленькие клиенты не отрезаются. Реализовано в
`LeadRouter::weightedPick` (вес `max(1, snapshot_daily_limit − delivered_today)`).

## Предусловия

- `DADATA_API_KEY` + `DADATA_SECRET` — завести в **YC Lockbox** (НЕ в git/.env репозитория).
  Прокинуть в окружение прод-воркеров (`DADATA_API_KEY`, `DADATA_SECRET`).
- Feature-flag `LEAD_REGION_RESOLVER_ENABLED` (по умолчанию `false` → текущее tag-поведение).
- Бюджет: `DADATA_DAILY_CAP_RUB` (дефолт 10000), `DADATA_CALL_COST_KOPECKS` (дефолт 60 —
  **откалибровать по фактическому тарифу DaData** после первого дня).

## Порядок выкатки

1. **Миграция БД.** Накатить `2026_05_31_100000_create_phone_ranges_and_resolution_log`
   (создаёт `phone_ranges`, `phone_ranges_imports`, `lead_region_resolution_log` +
   колонки в `supplier_leads`/`deals`). Партиции журнала на старте — m05/m06; далее
   их подхватывает `partitions:create-months` (уже зарегистрирован в `MonthlyPartitionManager`).
   - На проде миграция делает `SET ROLE crm_migrator` (паттерн проекта).
2. **Импорт реестра Россвязи.** Скачать пакет выписок с
   `rossvyaz.gov.ru/deyatelnost/resurs-numeracii/...` (~500-600 файлов) в каталог,
   затем `php artisan phone-ranges:import --dir=<каталог>`.
   - **NB парсер:** ожидает CSV `;`-разделитель, колонки `АВС/ DEF;От;До;Емкость;Оператор;Регион`.
     Реальные файлы Россвязи могут быть в cp1251 / иметь другие заголовки — сверить на
     первом импорте; при расхождении поправить `resolveColumns()` (это и есть первая
     боевая валидация — автотест покрывает CSV-фикстуру, не реальный формат).
   - **NB swap:** atomic RENAME (`phone_ranges` → `_old`, staging → `phone_ranges`) НЕ
     покрыт автотестом (коммитящий RENAME сломал бы общую тестовую БД). **Этот импорт —
     первая боевая проверка свапа.** Сначала прогнать `--dry-run` (staging без свапа),
     проверить `phone_ranges_staging` глазами, потом без `--dry-run`. Откат:
     `phone-ranges:rollback` (см. spec §6.4 — команда отката пока не реализована,
     при необходимости — ручной RENAME `phone_ranges_old` обратно).
3. **Деплой кода с `LEAD_REGION_RESOLVER_ENABLED=false`.** Резолвер выключен →
   поведение идентично текущему (tag-fallback). Каскад работает (но без точного
   региона, т.к. `resolved_subject_code=null` → шаг 2 «вся РФ» как раньше).
4. **Smoke на staging/проде:** `php artisan phone-region:smoke --phone=79161234567`
   (с реальным ключом — платный вызов, в БД не пишет). Проверить, что DaData отвечает,
   регион/оператор резолвятся, Россвязь-fallback находит префиксы. Прогнать §9.4 — ~100
   реальных prod-номеров, сверить распределение источников.
5. **Включить флаг (сразу 100%):** `LEAD_REGION_RESOLVER_ENABLED=true`. Рубильник
   глобальный — резолвер включается сразу для **всего** потока лидов. **Долевую
   (постепенную) раскатку НЕ делаем** (решение заказчика 01.06.2026): никакого
   `hash(phone) % 100`-гейта не вводим, фича идёт на 100% с первого включения.
6. **Мониторинг 1 день:** `lead_region_resolution_log` — распределение `region_source`
   (ожидание: dadata большинство, tag < 20%, unknown < 5% — spec §8.2). Проверить
   `DADATA_DAILY_CAP_RUB` не упирается. Откалибровать `DADATA_CALL_COST_KOPECKS`.
7. **Штатный режим:** фича уже работает на 100% потока (с шага 5) — долевого гейта нет,
   убирать нечего. Единственный рычаг управления — флаг `LEAD_REGION_RESOLVER_ENABLED`.
8. **Ежемесячный cron** импорта реестра (`phone-ranges:import`, 4-е число 03:00 МСК —
   spec §6.3) — добавить в планировщик/`artisan-run`.

## Откат

- Мгновенный: `LEAD_REGION_RESOLVER_ENABLED=false` → резолвер возвращает tag-fallback,
  каскад ведёт себя как до фичи. Код деплоить заново не нужно.
- Реестр: `phone_ranges_old` хранит предыдущую версию (ручной RENAME при проблеме импорта).

## Что отложено (followups, не блокируют ядро)

- **Метрики §8.1** (`phone_resolution.source.*` и т.д.) — в проекте нет механизма
  Prometheus/StatsD; отложено до его появления.
- **Долевая (постепенная) раскатка** — **НЕ делаем** (решение заказчика 01.06.2026):
  фича включается сразу на 100%, `hash(phone)%100`-гейт не вводится.
- **`phone-ranges:rollback`** — команда отката свапа (spec §6.4) не реализована.
- **`deals.region_source`** — не добавлялась (по спеке регион-источник живёт на
  `supplier_leads` + в журнале). CSV-merge (§3.12) обновляет регион сделки по
  эвристике «webhook dadata/rossvyaz > CSV-tag», без хранения source на сделке.
- **pg_anonymizer-маски (§7.2)** на `lead_region_resolution_log` — при настройке масок дампов.
- **152-ФЗ:** телефон в журнале маскирован (`7XXX***YYYY`), `dadata_response_masked`
  без сырого номера — базовое покрытие есть; полный аудит ПДн — через `pdn-152fz-audit`.
