Files
portal/docs/superpowers/plans/2026-05-31-lead-region-session-progress.md
T
2026-06-01 07:21:24 +03:00

15 KiB
Raw Blame History

Lead Region Resolution — прогресс автономного прогона (ночь 31.05.2026)

Хендофф после автономной ночной сессии. Вся работа на диске в worktree worktree-feat+lead-region-resolution, НЕ закоммичена (git commit/push требуют approval владельца через гейт — владелец спал). Утром: ревью → коммиты → продолжение.

Что сделано (Сессии 1–4 — весь движок резолва региона, TDD-зелёный)

Сессия Статус Тесты
1 Схема (миграция + партиции + schema.sql sync) на диске 9 passed / 27 assert
2 Россвязь (lookup + DTO + import-команда) на диске 9 passed / 27 assert
3 DaData (region map + config + enum + client + budget guard) на диске 16 passed / 119 assert
4 LeadRegionResolver (оркестратор, 16 кейсов каскада) на диске 16 passed / 46 assert
Консолидированная регрессия (все файлы вместе) 53 passed / 238 assert

Новые/изменённые файлы

Создано:

  • app/database/migrations/2026_05_31_100000_create_phone_ranges_and_resolution_log.php
  • app/app/Services/RossvyazPrefixLookup.php + app/app/Services/Dto/RossvyazRecord.php
  • app/app/Console/Commands/PhoneRangesImportCommand.php
  • app/app/Support/DaDataRegionMap.php
  • app/app/Services/DaData/{DaDataQualityCode,DaDataException,DaDataTimeoutException,DaDataPhoneResponse,DaDataPhoneClient,DaDataBudgetGuard}.php
  • app/app/Services/Dto/RegionResolution.php
  • app/app/Services/LeadRegionResolver.php
  • Тесты: tests/Feature/Migrations/PhoneRangesMigrationTest.php, tests/Unit/Services/MonthlyPartitionManagerRegionLogTest.php, tests/Feature/Services/RossvyazPrefixLookupTest.php, tests/Feature/Console/PhoneRangesImportCommandTest.php, tests/Unit/Support/DaDataRegionMapTest.php, tests/Feature/Services/DaData/{DaDataPhoneClientTest,DaDataBudgetGuardTest}.php, tests/Feature/Services/{RegionResolutionTest,LeadRegionResolverTest}.php
  • tests/Fixtures/rossvyaz/sample.csv

Изменено:

  • app/app/Services/MonthlyPartitionManager.php — +entry 'lead_region_resolution_log' => 'received_at'
  • app/app/Models/SupplierLead.php — +4 колонки в fillable + 2 int-cast
  • app/config/services.php — +блок dadata
  • app/tests/Feature/PartitionsCreateMonthsTest.php — хрупкий хардкод «48 skipped» → динамический count(PARTITIONED_TABLES) * 6
  • db/schema.sql (v8.39 → v8.40, только заголовок) + db/CHANGELOG_schema.md (+v8.40)

Решения, принятые по ходу (для ревью)

  1. Коды субъектов — по RussianRegions (Москва=82, СПб=83, МО=56, ЛО=53), НЕ по спеке (там были авто-коды 77/78/50/47 — неверно).
  2. GRANT'ы миграцииcrm_app_user + crm_supplier_worker (роли crm_readonly из плана не существует).
  3. schema.sql — только заголовок + CHANGELOG, без тела (как v8.39 project_routing_snapshots): иначе двойной CREATE TABLE (0001 грузит schema.sql + дельта-миграция) сломал бы migrate.
  4. Размещение тестов — app/DB-зависимые тесты (DaData-клиент, budget, resolver, DTO с моделью) лежат в tests/Feature/..., не tests/Unit/... как в плане: в проекте tests/Unit не бутит Laravel (нет Http::fake/app()/Cache). Чистый DaDataRegionMap остался в Unit.
  5. PhoneRangesImportCommand swap — atomic RENAME реализован по спеке, но committing-swap НЕ покрыт автотестом (RENAME коммитит и сломал бы общую liderra_testing, которую ночью без терминала владельца не пересоздать). Тесты покрывают parse/map/dry-run/idempotency/force. Свап проверяется первым реальным импортом оператора (Session 6 runbook). Косметика: lookup-индекс на новой таблице после свапа носит имя idx_phone_ranges_staging_lookup (имя idx_phone_ranges_lookup занято phone_ranges_old).
  6. DaData call costservices.dadata.call_cost_kopecks дефолт 60 (≈0.60 ₽/вызов) — прикидка, откалибровать по тарифу DaData.
  7. CSV-парсер импорта — нативный str_getcsv(';') (как проект читает файлы); реальный формат Россвязи (заголовки АВС/ DEF;От;До;Емкость;Оператор;Регион, возможно cp1251) уточняется оператором на реальном пакете. XLSX-ветка через openspout — не протестирована.

Что осталось (требует владельца)

Коммиты (утром, через git-approval)

Предлагаемая разбивка (conventional commits, ветка worktree-feat+lead-region-resolution):

  • feat(region): schema migration + MonthlyPartitionManager registration (миграция, partition manager, PartitionsCreateMonths fix, SupplierLead model, тесты Session 1)
  • chore(region): sync db/schema.sql + CHANGELOG (v8.40)
  • feat(region): RossvyazPrefixLookup + RossvyazRecord DTO
  • feat(region): phone-ranges:import command (parse/map/dry-run/idempotency)
  • feat(region): DaData layer (region map, config, enum, client, budget guard)
  • feat(region): LeadRegionResolver orchestrator (full qc cascade)

NB: коммит-сообщения без trailer Co-Authored-By — гейт блокирует символ < (угловые скобки email). Зафиксировано в docs/bugs.md.

D1 — продуктовое решение ДО Session 5

Сейчас при >3 кандидатах лид раздаётся 3 случайным клиентам. Каскад (Session 5) раздаёт 3 клиентам с наибольшим остатком дневного лимита (детерминированно) — клиент с большим остатком систематически получает больше лидов. Каскад по конструкции (роутер режет до 3 упорядоченно → LeadDistributor не шаффлит) и есть эта смена. Нужно подтверждение: убрать random — ок? (Если хочешь сохранить случайность внутри региона — это +1 задача: shuffle внутри каждой фазы перед cap.)

Session 5 (каскад LeadRouter) + Session 6 (интеграция в Job) — после D1

  • Зависят от D1 + трогают прод-критичный RouteSupplierLeadJob (30k лидов/сутки) → делать с ревью, не вслепую.
  • Session 6 Task 6.4 (smoke-команда phone-region:smoke) + метрики §8 — отдельно.

Pre-existing tech debt (не моё, флагую)

  • tests/Feature/Import/MonthlyPartitionManagerTest.php::ensureMonth создаёт партицию webhook_logкрасный независимо от меня: webhook_log удалён из проекта 24.05 (миграция 2026_05_24_140000), тест не обновили. Можно убрать как наследие отдельным мелким фиксом — на твоё усмотрение.
  • migrate:fresh на проекте сломан (cross-PDO auth_log в миграции 0001): миграция грузит schema.sql на pgsql, затем зовёт partitions:create-months на pgsql_supplier в той же транзакции → невидимость. Тестовая база liderra_testing собрана клоном dev (CREATE DATABASE ... WITH TEMPLATE liderra), а не через migrate:fresh. Отдельная проблема, вне фичи.

Как прогнать (из app/)

vendor/bin/pest tests/Feature/Migrations/PhoneRangesMigrationTest.php tests/Unit/Services/MonthlyPartitionManagerRegionLogTest.php tests/Feature/Services/RossvyazPrefixLookupTest.php tests/Feature/Console/PhoneRangesImportCommandTest.php tests/Unit/Support/DaDataRegionMapTest.php tests/Feature/Services/DaData tests/Feature/Services/RegionResolutionTest.php tests/Feature/Services/LeadRegionResolverTest.php

→ 53 passed / 238 assertions.


ОБНОВЛЕНИЕ 01.06.2026 — Сессии 5–6 реализованы, фича функционально завершена

D1 решён заказчиком — вариант В (взвешенный жребий по остатку лимита; мелкие клиенты не отрезаются, вес ≥ 1 у каждого).

Сессия Что сделано Тесты
5 LeadRouter каскад (exact→all-RF→fallback) + взвешенный жребий (В) + routing_step LeadRouter переписан: matchEligibleProjects($sp, ?int $resolvedSubjectCode), queryCandidates (region-фильтр + snap.regions), weightedPick, инъекция Randomizer. Хелпер createRoutingSnapshotFromProject(+regions). 9 cascade + 10 regression
6.1 Резолв до tx + persist + лог в RouteSupplierLeadJob app(LeadRegionResolver)->resolve() (НЕ 7-й параметр handle — чтобы не ломать сигнатуру/тесты), persist 4 колонки, logRegionResolution (fail-safe INSERT в журнал через pgsql_supplier, маскированный телефон). в наборе из 8
6.2 Подмена subject_code на шаге 3 + region_substituted createDealCopyForProject(RegionResolution), routing_step захватывается до $lockedProject, pickSubstituteRegion(snapshot.regions). Deal +phone_operator/region_substituted (model fillable+cast). в наборе из 8
6.3 CSV-merge по рангу источника merge-блок обновляет subject_code/phone_operator если webhook-резолв dadata/rossvyaz (выше tag CSV). Эвристикаdeals.region_source нет (документировано). 2
6.4 Smoke-команда phone-region:smoke резолв по телефону без записи в БД. Метрики §8.1 отложены (нет механизма Prometheus/StatsD в проекте). 2
6.5 Финальная регрессия + runbook 101 passed / 509 assertions (вся фича + регрессия Job ×3 / Router ×2). Runbook раскатки: docs/superpowers/runbooks/2026-05-31-lead-region-resolution-rollout.md. 101

Новые/изменённые файлы Сессий 5–6 (в worktree, не закоммичено)

  • Изменено: app/app/Services/LeadRouter.php (каскад + weighted pick + Randomizer), app/app/Jobs/RouteSupplierLeadJob.php (resolve+persist+log+substitution+CSV-merge), app/app/Models/Deal.php (+2 fillable, +1 cast), app/tests/Pest.php (helper +regions).
  • Создано: app/app/Console/Commands/PhoneRegionSmokeCommand.php; тесты LeadRouterCascadeTest.php, RouteSupplierLeadJobRegionResolutionTest.php, PhoneRegionSmokeCommandTest.php; runbook.

Решения Сессий 5–6 (для ревью)

  1. D1=В — взвешенный жребий, мелкие не отрезаны (доказано тестом variant В: weighted pick — 120 seed'ов, мелкий выигрывает >0 раз, крупный чаще).
  2. LeadRegionResolver через app() внутри handle(), не 7-м параметром — иначе ломались бы сигнатура + 3 существующих Job-теста.
  3. Лог резолва fail-safe — сбой записи аудит-лога не роняет доставку лида (30k/сутки).
  4. deals.region_source НЕ добавлялась — CSV-merge по рангу через эвристику (dadata/rossvyaz > CSV-tag). Отклонение от плана Task 6.3 (план предполагал колонку), задокументировано.
  5. Метрики §8.1 отложены — нет механизма метрик в проекте.

Коммиты Сессий 5–6 (предложение, ветка worktree-feat+lead-region-resolution)

  • test(region): createRoutingSnapshotFromProject accepts regions param
  • feat(region): LeadRouter cascade routing (exact→all-RF→fallback) + weighted pick variant В + routing_step
  • feat(region): wire LeadRegionResolver into RouteSupplierLeadJob + persist + fail-safe log
  • feat(region): step-3 region substitution + CSV-merge by source rank
  • feat(region): phone-region:smoke staging command
  • docs(region): rollout runbook + session progress

Пре-существующий долг (флагую, не моё)

  • tests/Feature/Console/{BillingMigrateLeadsToRub,IncidentsWatchFailures,SnapshotBackfillCommand}Testвзаимно загрязняются при прогоне в одном процессе (счётчики растут: ожидал 1, получил 4-5). Падают и БЕЗ моих файлов. В реальном CI (pest --parallel, файл = процесс) проходят. Тест-изоляция этих команд хрупкая — отдельная задача.

Команда финальной регрессии (явный список, из app/)

vendor/bin/pest tests/Feature/Migrations/PhoneRangesMigrationTest.php tests/Unit/Services/MonthlyPartitionManagerRegionLogTest.php tests/Feature/Services/RossvyazPrefixLookupTest.php tests/Feature/Console/PhoneRangesImportCommandTest.php tests/Feature/Console/PhoneRegionSmokeCommandTest.php tests/Unit/Support/DaDataRegionMapTest.php tests/Feature/Services/DaData tests/Feature/Services/RegionResolutionTest.php tests/Feature/Services/LeadRegionResolverTest.php tests/Feature/Services/LeadRouterTest.php tests/Feature/Services/LeadRouterCascadeTest.php tests/Feature/Jobs/RouteSupplierLeadJobRegionResolutionTest.php tests/Feature/Jobs/RouteSupplierLeadJobTest.php tests/Feature/Jobs/RouteSupplierLeadJobSnapshotTest.php tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php

→ 101 passed / 509 assertions.