15 KiB
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.phpapp/app/Services/RossvyazPrefixLookup.php+app/app/Services/Dto/RossvyazRecord.phpapp/app/Console/Commands/PhoneRangesImportCommand.phpapp/app/Support/DaDataRegionMap.phpapp/app/Services/DaData/{DaDataQualityCode,DaDataException,DaDataTimeoutException,DaDataPhoneResponse,DaDataPhoneClient,DaDataBudgetGuard}.phpapp/app/Services/Dto/RegionResolution.phpapp/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-castapp/config/services.php— +блокdadataapp/tests/Feature/PartitionsCreateMonthsTest.php— хрупкий хардкод «48 skipped» → динамическийcount(PARTITIONED_TABLES) * 6db/schema.sql(v8.39 → v8.40, только заголовок) +db/CHANGELOG_schema.md(+v8.40)
Решения, принятые по ходу (для ревью)
- Коды субъектов — по
RussianRegions(Москва=82, СПб=83, МО=56, ЛО=53), НЕ по спеке (там были авто-коды 77/78/50/47 — неверно). - GRANT'ы миграции —
crm_app_user+crm_supplier_worker(ролиcrm_readonlyиз плана не существует). schema.sql— только заголовок + CHANGELOG, без тела (как v8.39 project_routing_snapshots): иначе двойнойCREATE TABLE(0001 грузит schema.sql + дельта-миграция) сломал быmigrate.- Размещение тестов — app/DB-зависимые тесты (DaData-клиент, budget, resolver, DTO с моделью) лежат в
tests/Feature/..., неtests/Unit/...как в плане: в проектеtests/Unitне бутит Laravel (нетHttp::fake/app()/Cache). ЧистыйDaDataRegionMapостался в Unit. PhoneRangesImportCommandswap — 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).- DaData call cost —
services.dadata.call_cost_kopecksдефолт 60 (≈0.60 ₽/вызов) — прикидка, откалибровать по тарифу DaData. - 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 DTOfeat(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-PDOauth_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 (для ревью)
- D1=В — взвешенный жребий, мелкие не отрезаны (доказано тестом
variant В: weighted pick— 120 seed'ов, мелкий выигрывает >0 раз, крупный чаще). - LeadRegionResolver через
app()внутриhandle(), не 7-м параметром — иначе ломались бы сигнатура + 3 существующих Job-теста. - Лог резолва fail-safe — сбой записи аудит-лога не роняет доставку лида (30k/сутки).
deals.region_sourceНЕ добавлялась — CSV-merge по рангу через эвристику (dadata/rossvyaz > CSV-tag). Отклонение от плана Task 6.3 (план предполагал колонку), задокументировано.- Метрики §8.1 отложены — нет механизма метрик в проекте.
Коммиты Сессий 5–6 (предложение, ветка worktree-feat+lead-region-resolution)
test(region): createRoutingSnapshotFromProject accepts regions paramfeat(region): LeadRouter cascade routing (exact→all-RF→fallback) + weighted pick variant В + routing_stepfeat(region): wire LeadRegionResolver into RouteSupplierLeadJob + persist + fail-safe logfeat(region): step-3 region substitution + CSV-merge by source rankfeat(region): phone-region:smoke staging commanddocs(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.