Слияние конкурентов больше не «чёрная дыра» (баг «САПС/Метрополис»: карточка
исчезла без следа, вернуть было нечем). Теперь каждое слияние пишет событие в
autopodbor_merge_events со СНИМКОМ поглощённых карточек (имя/сайт/телефоны/
справочники), кто и когда слил.
- Таблица autopodbor_merge_events (+RLS tenant_isolation, паттерн 1:1 с sources,
rls-ревью OK; nullOnDelete на survivor_id/user_id — история не рушится).
- AutopodborCompetitorMerger::merge($tenantId,$ids,$name,$userId) пишет событие.
- GET /field/merge-events — история; POST /field/merge-events/{id}/restore —
возврат поглощённых как предложения (защита от повторного возврата).
- Фронт: панель «🕘 История слияний» + «↩ Вернуть карточку».
Бэк 428/428, фронт 12/12, Pint чистый, build ок. schema.sql канон-синк всех
таблиц автоподбора — предвыкатный TODO (вся фича migration-only в этой ветке).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Две починки в «Конкурентном поле» + превью выжившего в окне склейки:
1. Переименование конкурента (PATCH competitors/{id}) не обновляло имена
уже созданных проектов — они носят имя-снимок конкурента. Теперь при
смене имени проекты синхронизируются (syncNamesForCompetitor): меняется
только отображаемое имя, signal_identifier поставщика не трогаем.
2. Слияние дублей не пересматривало ящик выжившего. Если выжившим по
возрасту/деньгам оказывалось предложение, объединённая карточка уезжала
в «Предложения», даже когда одна из склеиваемых была в «Поле». Теперь
объединённая получает сильнейший ящик (поле > предложение > архив) —
только повышаем, никогда не понижаем.
3. Окно «Почистить поле» показывает, какая карточка останется после склейки
(survivor во duplicateGroups) — не затираем имя вслепую.
Тесты: весь блок автоподбора зелёный (426). Larastan в этой рабочей копии
падает без вывода на любом файле — среда, не код.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Имя проекта следует за конкурентом: после склейки дублей все проекты объединённого
получают имя выжившего конкурента, при совпадении различаем источником, а не цифрой.
Проекты не удаляются и не рвутся — меняется только имя.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Диалог дублей: селект имени среди выбранных к слиянию, по умолчанию первое.
Merger и ручка принимают опциональное имя. Обе экрана.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Конкурент хранит список элементов {name,sites,cards} - подкомпании/ЖК холдинга.
Слияние складывает элементы всех объединяемых, ничего не роняет.
Миграция бэкфилла: один элемент из текущих site_url + directory_urls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Шаг 1 «Конкурентное поле» — чекпоинт:
- Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча.
- Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента.
- Колонка phones jsonb; телефон-дубли видны уже на «Предложениях».
- Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт».
- Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание.
- Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм.
Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>