docs(автоподбор): дизайн-документ + план реализации
Дизайн и пошаговый план фичи «Автоподбор конкурентов» (ИИ-агент находит конкурентов и их источники). Движок — отдельной сессией, здесь розетка+заглушка. План сверен с кодом: RLS app.current_tenant_id, tenant-контекст SET LOCAL, тестовая БД liderra_testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
# Автоподбор конкурентов — дизайн-документ
|
||||
|
||||
> **Дата:** 2026-06-28. **Статус:** черновик на ревью владельца.
|
||||
> **Первоисточники брейншторма** (не пересказ — читать вместе с этим документом):
|
||||
> - `2026-06-28-autopodbor-konkurentov-brainstorm-notes.md` — все решения и хвосты.
|
||||
> - `2026-06-28-autopodbor-konkurentov-prototype.html` — согласованный UX «под ключ».
|
||||
> - `2026-06-28-autopodbor-konkurentov-NEXT-SESSION-PROMPT.md` — сжатый контекст.
|
||||
>
|
||||
> **Граница этой работы (решение владельца 2026-06-28):** сам ИИ-движок (как реально
|
||||
> ходим в интернет) делается в ОТДЕЛЬНОЙ сессии. Здесь проектируется и собирается
|
||||
> ВСЁ остальное так, чтобы готовый движок вставился в одну заранее описанную «розетку»
|
||||
> (§7) без переделки экранов, данных и денег. На v1 за движок работает заглушка.
|
||||
|
||||
---
|
||||
|
||||
## 0. Для владельца (без программистских слов)
|
||||
|
||||
Что строим: новую вкладку «Автоподбор конкурентов» в кабинете клиента. Клиент даёт
|
||||
несколько своих конкурентов и регион — система находит похожих, по выбранным
|
||||
вытаскивает их источники (сайты и телефоны), а клиент одной кнопкой заводит из них
|
||||
проекты. Обе тяжёлые операции (подбор и изучение) — платные, деньги списываем
|
||||
**только за успешный результат**, всё считается в фоне, результаты сохраняются, за уже
|
||||
оплаченное второй раз не берём.
|
||||
|
||||
Что в этой работе НЕ делаем: «мозг», который реально ищет в интернете. Его точите
|
||||
отдельно. Мы готовим всё вокруг и оставляем «розетку», куда мозг потом вставится.
|
||||
Пока розетка занята заглушкой (выдаёт заготовленные данные), чтобы экраны, деньги и
|
||||
сохранение можно было собрать и проверить целиком уже сейчас.
|
||||
|
||||
**Что я решил по умолчанию за тебя** (помечено ниже значком 🟡 — поправь на ревью):
|
||||
- 🟡 Режим «свой конкурент по названию» — добавлен экран подтверждения «мы нашли вот
|
||||
эту компанию — она?» ДО списания денег (§3.3).
|
||||
- 🟡 Пустой результат — экран «ничего не нашли», деньги не списываем (§3.7, §5).
|
||||
- 🟡 Источники — только сайт + телефон, SMS не трогаем (§4.2).
|
||||
- 🟡 Объём: шаг 1 — до 15 конкурентов, шаг 2 — без потолка но с дедупом; всё
|
||||
настраивается в админке (§6).
|
||||
- 🟡 Цена — задаётся в админке, пока не задана и фича не включена — вкладка скрыта.
|
||||
Я не выдумываю сумму (§5.4).
|
||||
|
||||
---
|
||||
|
||||
## 1. Объём (scope) и принципы
|
||||
|
||||
**Входит в v1:**
|
||||
1. Вкладка «Автоподбор конкурентов» (пункт меню в группе «Работа», метка NEW).
|
||||
2. Экран входа с блоком «Продолжить начатое» + две двери (авто-подбор / свой конкурент).
|
||||
3. Форма авто-подбора (примеры конкурентов, «о себе», регион, федеральные).
|
||||
4. Форма «свой конкурент» (+ экран подтверждения найденной по названию компании).
|
||||
5. Фоновые платные прогоны: «подбор» (шаг 1) и «изучить» (шаг 2), с сохранением
|
||||
состояния, идемпотентностью и списанием «только за успех».
|
||||
6. Экран списка конкурентов с % похожести и поштучным прогрессом.
|
||||
7. Экран источников конкурента (живое состояние, дедуп, «проект создан», «Изменить проект»).
|
||||
8. Экран создания проектов (общие настройки + префлайт баланса) и экран «Готово».
|
||||
9. Админ-настройки: вкл/выкл фичи и цены за оба шага.
|
||||
10. «Розетка» движка: интерфейс агента + заглушка (`Fake`) на время отсутствия мозга.
|
||||
|
||||
**НЕ входит в v1 (явно):**
|
||||
- Реальный ИИ-движок с выходом в интернет (отдельная сессия).
|
||||
- Уведомления по почте (только в портале — значок/тост; решение владельца).
|
||||
- Рекомендатель источников поставщика BG (владелец велел не использовать).
|
||||
- SMS как тип источника фичи (только сайт/звонок).
|
||||
- Черновик частично заполненной формы создания (источники не теряются — этого хватает).
|
||||
|
||||
**Сквозные принципы:**
|
||||
- Деньги — чувствительная зона: списываем атомарно, идемпотентно, только за успех
|
||||
(§5). Любая правда о деньгах сверяется billing-audit.
|
||||
- Никаких дублей на трёх уровнях: конкуренты, источники, проекты клиента (§4.4).
|
||||
- Все результаты агента — кандидаты; клиент проверяет по ссылкам-провенансам (§7.4).
|
||||
- RLS: все новые таблицы — tenant-isolated (§4.1).
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектура (карта частей)
|
||||
|
||||
```
|
||||
Кабинет клиента (Vue) Backend (Laravel) Движок (розетка)
|
||||
───────────────────── ───────────────── ────────────────
|
||||
AutopodborView.vue ───POST───► AutopodborController
|
||||
├ вход + история ├ старт прогона ──► AutopodborRunService
|
||||
├ формы (2 режима) │ (создаёт run, ставит job)
|
||||
├ список конкурентов │
|
||||
├ источники ├ чтение run/result (опрос статуса)
|
||||
└ создание проектов ──POST──► └ создание проектов ──► ProjectService (как сейчас)
|
||||
|
||||
Очередь (Redis) CompetitorAgent (interface) ← §7
|
||||
│ ├ FakeCompetitorAgent (v1)
|
||||
RunAutopodborSearchJob ──вызывает──► └ RealCompetitorAgent (потом)
|
||||
RunAutopodborStudyJob ──вызывает──►
|
||||
│
|
||||
пишет результат + при успехе списывает деньги (LedgerService-путь, §5)
|
||||
```
|
||||
|
||||
**Единицы и их ответственность (для изоляции и тестируемости):**
|
||||
|
||||
| Единица | Что делает | Зависит от |
|
||||
|---|---|---|
|
||||
| `AutopodborController` | HTTP: старт прогонов, чтение состояния, запуск создания проектов | RunService, ProjectService |
|
||||
| `AutopodborRunService` | жизненный цикл прогона: создать run, гейт баланса, поставить job, идемпотентность (один in-flight на tenant) | models, BalancePreflightService |
|
||||
| `RunAutopodborSearchJob` | фоновый шаг 1: дернуть агента, сохранить конкурентов, при успехе списать | CompetitorAgent, ChargeService |
|
||||
| `RunAutopodborStudyJob` | фоновый шаг 2 (и manual-study): дернуть агента, сохранить источники, при успехе списать | CompetitorAgent, ChargeService |
|
||||
| `CompetitorAgent` (iface) | «розетка»: поиск конкурентов + изучение одного + резолв по названию | — |
|
||||
| `FakeCompetitorAgent` | v1-заглушка: заготовленные данные, без интернета | — |
|
||||
| `AutopodborChargeService` | списание за прогон: атомарно, идемпотентно по run_id, новый тип проводки | LedgerService-паттерн |
|
||||
| `AutopodborDedup` | нормализация и дедуп конкурентов/источников; «уже есть проект» | ProjectService::assertSourceUnique-логика |
|
||||
| `ProjectService` (есть) | создание проектов из источников — БЕЗ изменений в ядре | — |
|
||||
|
||||
Каждую единицу можно понять и протестировать отдельно; контракт между ними —
|
||||
типизированные DTO (§7.2).
|
||||
|
||||
---
|
||||
|
||||
## 3. Экраны и потоки (по прототипу)
|
||||
|
||||
Нумерация экранов = id из прототипа. UX согласован; ниже — поведение и привязка к данным.
|
||||
|
||||
### 3.1. Вход (`s-entry`)
|
||||
- Блок **«Продолжить начатое»** — если есть незавершённые/оплаченные прогоны (см. §4.1
|
||||
`autopodbor_runs`). Каждая строка: регион, дата, найдено N, изучено M, проектов K,
|
||||
«оплачено». Клик «Открыть» → в нужную точку (список или источники), БЕЗ повторной оплаты.
|
||||
Если истории нет — блок не показываем.
|
||||
- Две двери: «Подобрать конкурентов» (→ `s-autoform`), «Указать своего конкурента»
|
||||
(→ `s-manualform`). Обе — первоклассные, не «запасной выход».
|
||||
|
||||
### 3.2. Форма авто-подбора (`s-autoform`)
|
||||
- **Ваши конкуренты** (обязательно, ≥1; подсказка «чем больше — тем точнее»): строки
|
||||
«сайт или ссылка на справочник», кнопка «+ добавить конкурента».
|
||||
- **О вашей компании** (по желанию): несколько сайтов, несколько справочников, описание
|
||||
деятельности (textarea с подсказкой что описывать). Тон — деловой, уважительный.
|
||||
- **Регион** — обязателен, ровно один (не мультивыбор).
|
||||
- Галка **«включать федеральных игроков»** (работают в регионе клиента И в других).
|
||||
- Кнопка «Подобрать конкурентов» → окно подтверждения цены (§5.3) → фоновый шаг 1
|
||||
(`s-loading` → `s-list`).
|
||||
|
||||
### 3.3. Форма «свой конкурент» (`s-manualform`) + 🟡 подтверждение по названию
|
||||
- Ввод: сайт/ссылка на справочник ИЛИ только название; регион (один, обязателен).
|
||||
- Кнопка «Собрать источники» → подтверждение цены (§5.3).
|
||||
- **Развилка (🟡 новое, дыра прототипа):**
|
||||
- дан сайт/справочник → агент копает сразу → `s-loading` → `s-detail`.
|
||||
- дано только название → СНАЧАЛА дешёвый резолв (`CompetitorAgent::resolveByName`,
|
||||
бесплатно/в рамках того же прогона) → экран **«Мы нашли вот эту компанию — она?»**
|
||||
(название, описание, сайт/справочник, провенанс) → клиент жмёт «Да, изучить» → ТОЛЬКО
|
||||
тогда списываем за тяжёлый шаг и копаем источники. «Не та» → назад, без списания.
|
||||
- Если по названию найдено несколько кандидатов — показываем список на выбор.
|
||||
- Если не найдено никого — экран «не нашли» (§3.7), без списания.
|
||||
|
||||
### 3.4. Список конкурентов (`s-list`)
|
||||
- Заголовок «Найдено N конкурентов» + recap-пиллы (регион, федеральные, по скольким примерам).
|
||||
- Сортировка: 100% → ниже (релевантность = похожесть на ПРИМЕРЫ клиента, не на регион).
|
||||
- Карточка: название, метки регион/«федеральный», описание, ссылки (Сайт + Справочники),
|
||||
число % похожести.
|
||||
- **Изученный** конкурент → метка «✓ изучен» + «Открыть источники» (БЕСПЛАТНО).
|
||||
- **Неизученный** → «Изучить подробнее» (платно) → подтверждение цены → шаг 2.
|
||||
- Внизу: «Не нашли нужного? Указать вручную» → `s-manualform`.
|
||||
|
||||
### 3.5. Источники конкурента (`s-detail`)
|
||||
- Шапка: имя + метка региона + % похожести; «Изучено DD.MM · найдено K источников».
|
||||
- Пояснение про настоящий/подменный + что состояние живое.
|
||||
- Секции: **🌐 Сайты** (только головы доменов) и **📞 Телефоны** (метка
|
||||
`настоящий` ✓ / `подменный · с сайта` 🎭). У каждого источника — чекбокс + провенанс-ссылка
|
||||
«Где нашли».
|
||||
- **Живое состояние (§4.4):** источник, по которому уже есть проект клиента → строка
|
||||
заблокирована, метка «✓ проект создан», кнопка «Изменить проект» (→ `s-editproject`).
|
||||
- «Чего-то не хватает? Добавить источник вручную» (сайт/номер).
|
||||
- Нижняя панель: «Выбрано X из K», «Создать проекты →» (→ `s-create`).
|
||||
|
||||
### 3.6. Создание проектов (`s-create`) и «Изменить проект» (`s-editproject`)
|
||||
- Список «Будет создано N проектов»: слева тип+идентификатор источника (со значком ✓/🎭
|
||||
у телефонов), справа редактируемое поле **Название проекта** (автоимя = имя конкурента,
|
||||
значок запекается в текст — см. §4.5).
|
||||
- Блок «Настройки проектов»: общий регион (из подбора), общий лимит/день, дни приёма.
|
||||
Применяются ко всем; после создания каждый правится в «Проектах».
|
||||
- Прикидка стоимости запуска + строка баланса «хватает ✓».
|
||||
- Кнопки: «Создать (без запуска)» и «Создать и запустить →» (с префлайтом баланса, §5.5).
|
||||
- `s-editproject`: правка одного существующего проекта (имя/регион/лимит/дни) прямо из
|
||||
источников, через тот же `PATCH /api/projects/{id}`.
|
||||
|
||||
### 3.7. Состояния-исходы
|
||||
- **Загрузка** (`s-loading`): спиннер + «можно закрыть вкладку, сохраним результат,
|
||||
деньги — только за успех». Опрос статуса прогона по `GET` (§4.3).
|
||||
- **Готово** (`s-done`): «N проектов создано/запущено», ссылки в «Проекты» и «В начало».
|
||||
- 🟡 **Пусто/ошибка** (новый экран `s-empty`): «Ничего не нашли по этому запросу. Деньги
|
||||
не списаны». Кнопки «Изменить запрос» / «В начало». Показывается, когда прогон
|
||||
завершился `empty` или `failed` (§5.2).
|
||||
|
||||
---
|
||||
|
||||
## 4. Данные
|
||||
|
||||
### 4.1. Новые таблицы (все RLS tenant-isolated; миграции + запись в `db/CHANGELOG_schema.md`)
|
||||
|
||||
**`autopodbor_runs`** — один платный прогон (или резолв по названию).
|
||||
| колонка | тип | смысл |
|
||||
|---|---|---|
|
||||
| id | bigint PK | |
|
||||
| tenant_id | bigint FK | RLS |
|
||||
| kind | enum(`search`,`study`,`resolve`) | шаг 1 / шаг 2 / резолв по названию |
|
||||
| status | enum(`queued`,`running`,`done`,`empty`,`failed`) | жизненный цикл |
|
||||
| region_code | int (1..89) | регион подбора |
|
||||
| params | jsonb | вход: примеры, «о себе», federal, имя/сайт конкурента |
|
||||
| competitor_id | bigint FK null | для `study` — какого конкурента изучаем |
|
||||
| price_rub_charged | numeric(12,2) null | сколько реально списали (только при `done`) |
|
||||
| balance_transaction_id | bigint FK null | проводка списания (§5) |
|
||||
| error_code | varchar null | для `failed` |
|
||||
| created_at / started_at / finished_at | timestamptz | |
|
||||
|
||||
**`autopodbor_competitors`** — конкуренты из прогона (авто-подбор + manual + resolve).
|
||||
| колонка | тип | смысл |
|
||||
|---|---|---|
|
||||
| id, tenant_id | | RLS |
|
||||
| search_run_id | bigint FK null | из какого подбора (null для «свой конкурент») |
|
||||
| name | varchar | |
|
||||
| description | text | |
|
||||
| is_federal | bool | метка «федеральный» |
|
||||
| relevance_pct | smallint null | похожесть на примеры (null для manual/resolve) |
|
||||
| origin | enum(`auto`,`manual`,`resolve`) | как попал |
|
||||
| site_url | varchar null | голова домена |
|
||||
| directory_urls | jsonb | ссылки на 2ГИС/Яндекс.Карты |
|
||||
| provenance | jsonb | где нашли (на уровне конкурента) |
|
||||
| dedup_key | varchar | нормализованный ключ (для дедупа конкурентов) |
|
||||
| study_run_id | bigint FK null | прогон изучения (если изучен) |
|
||||
| studied_at | timestamptz null | изучен → открывается бесплатно |
|
||||
| created_at | | |
|
||||
|
||||
**`autopodbor_sources`** — источники конкурента (результат шага 2).
|
||||
| колонка | тип | смысл |
|
||||
|---|---|---|
|
||||
| id, tenant_id | | RLS |
|
||||
| competitor_id | bigint FK | |
|
||||
| study_run_id | bigint FK | |
|
||||
| signal_type | enum(`site`,`call`) | SMS не используется |
|
||||
| identifier | varchar | нормализован: голова домена / `7xxxxxxxxxx` |
|
||||
| phone_kind | enum(`real`,`substitute`) null | для телефона; null для сайта |
|
||||
| provenance_url | varchar | ссылка «где нашли» |
|
||||
| provenance_label | varchar | человекочитаемо («2ГИС — карточка», «футер сайта») |
|
||||
| dedup_key | varchar | `signal_type` + нормализованный `identifier` |
|
||||
| created_project_id | bigint FK null | если по источнику создан проект (живое состояние) |
|
||||
| created_at | | |
|
||||
|
||||
Дедуп внутри прогона — UNIQUE `(competitor_id, dedup_key)`; конкуренты —
|
||||
UNIQUE `(tenant_id, COALESCE(search_run_id,0), dedup_key)`.
|
||||
|
||||
### 4.2. Источники = только сайт + телефон (🟡)
|
||||
SMS как тип источника фичи не поддерживаем (notes 6,7). `signal_type` источника ∈
|
||||
{`site`,`call`}. На создании проектов это маппится на существующие site/call.
|
||||
|
||||
### 4.3. Опрос статуса
|
||||
`GET /api/autopodbor/runs/{id}` → `{status, ...}`. Фронт на `s-loading` опрашивает раз в
|
||||
~2–3 c до `done`/`empty`/`failed`. Возврат на вкладку позже — состояние из БД, не из памяти.
|
||||
|
||||
### 4.4. Дедуп на трёх уровнях (жёсткое требование, заострено дважды)
|
||||
1. **Конкуренты:** `AutopodborDedup` нормализует имя/домен → `dedup_key`; не показываем
|
||||
дубль и не показываем уже изученного/заведённого повторно.
|
||||
2. **Источники:** нормализация (голова домена; телефон `7xxxxxxxxxx`) → дедуп внутри
|
||||
конкурента и между конкурентами одного прогона.
|
||||
3. **Проекты клиента (живая проверка при КАЖДОМ показе `s-detail`):** для каждого источника
|
||||
запрос к `projects` по `(tenant_id, signal_type, signal_identifier)` — та же логика,
|
||||
что в `ProjectService::assertSourceUnique()`. Есть проект → метка «✓ проект создан»,
|
||||
выбор заблокирован, кнопка «Изменить проект». Это не stale-снимок: проверяется на лету.
|
||||
|
||||
### 4.5. Имя проекта и значок (notes 6k)
|
||||
- Автоимя проекта = **имя конкурента** (без источника в названии).
|
||||
- Значок типа телефона запекается в текст имени: «Окна Комфорт ✓» / «Окна Комфорт 🎭».
|
||||
Для сайта значка нет.
|
||||
- **Уникальность имени** (`assertNameUnique`, 422 при дубле): несколько источников одного
|
||||
конкурента дадут одинаковые имена. Решение: при создании пачки добавлять тихий
|
||||
различитель — числовой суффикс « 2», « 3»… по факту коллизии (проверять перед вставкой,
|
||||
не ломая UX). Клиент потом переименует.
|
||||
- **Эмодзи поставщику:** имя реально уходит в `SupplierProjectDto.name` →
|
||||
`manage-project.js` (field `name`). Эмодзи может не пройти кодировку BG. Поэтому:
|
||||
значок держим ТОЛЬКО на стороне Лидерры (показ в «Проектах»), а в DTO имени —
|
||||
`strip` эмодзи перед отправкой. Хвост-проверка: принимает ли BG юникод (§9).
|
||||
|
||||
---
|
||||
|
||||
## 5. Деньги и биллинг (зона billing-audit)
|
||||
|
||||
### 5.1. Модель «деньги только за успех»
|
||||
Платные прогоны — только `search` (шаг 1) и `study` (шаг 2/manual-study). Прогон
|
||||
`resolve` (резолв конкурента по названию) — **бесплатный** (`price_rub_charged` всегда
|
||||
null); деньги за изучение берём отдельным `study`-прогоном уже после подтверждения
|
||||
найденной компании (§3.3). В коде НЕТ механизма резерва/hold (есть только прямое
|
||||
списание `LedgerService::chargeForDelivery`). Поэтому для платных прогонов:
|
||||
- При старте прогона деньги **НЕ списываются**. Прогон создаётся в статусе
|
||||
`queued`→`running`; для клиента это «в работе» (UX-«резерв»).
|
||||
- **Гейт перед стартом:** проверяем, что баланса хватает на цену шага (read-only). Не
|
||||
хватает → не стартуем, показываем «недостаточно средств» (без списания).
|
||||
- **Списание — атомарно и только при `done`** (есть непустой результат): внутри
|
||||
`DB::transaction` с `lockForUpdate` по tenant — по образцу `LedgerService`:
|
||||
пишем `BalanceTransaction` нового типа + проставляем `autopodbor_runs.price_rub_charged`
|
||||
и `balance_transaction_id`.
|
||||
- `empty`/`failed`/timeout → **ничего не списываем** (UX-«резерв» снят). Без «возвратов».
|
||||
|
||||
### 5.2. Идемпотентность (защита от двойного списания)
|
||||
- **Один in-flight прогон на tenant** (notes 6b.3): `AutopodborRunService` не стартует
|
||||
второй `queued/running` прогон того же `kind` для tenant; кнопка на фронте заблокирована;
|
||||
refresh не плодит второй прогон.
|
||||
- Списание идемпотентно **по `run_id`**: если у прогона уже есть `balance_transaction_id`
|
||||
— повторное списание невозможно (проверка внутри транзакции). Это исключает гонку и
|
||||
двойной debit при ретрае джобы.
|
||||
- Поскольку in-flight один и debit — в конце, риск конкурентного овердрафта ограничен
|
||||
единственным прогоном. (Альтернатива — настоящий hold-резерв в леджере — отложена;
|
||||
при росте нагрузки вернуться. Помечено для billing-audit.)
|
||||
|
||||
### 5.3. Новый тип проводки
|
||||
`BalanceTransaction` получает тип `TYPE_AUTOPODBOR_CHARGE = 'autopodbor_charge'`
|
||||
(`amount_rub` отрицательный, `related_type`/`related_id` → `autopodbor_runs`). Добавить в
|
||||
константы модели + в любые отчёты/фильтры по типам. Запись в `db/CHANGELOG_schema.md`,
|
||||
ревью billing-audit на money-инварианты (сумма проводок = изменение баланса).
|
||||
|
||||
### 5.4. Цена и вкл/выкл — через `system_settings` (🟡)
|
||||
Паттерн как у `billing_yookassa_enabled`. Ключи:
|
||||
- `autopodbor_enabled` (bool, default `false`) — пока выключено, вкладка/пункт меню скрыты.
|
||||
- `autopodbor_price_search_rub` (decimal) — цена шага 1.
|
||||
- `autopodbor_price_study_rub` (decimal) — цена шага 2 (и manual-study).
|
||||
Чтение — `SystemSettings::get/bool`. Редактирование — существующий
|
||||
`AdminSystemSettingsController` + `SystemSettingEditDialog.vue` (с reason ≥30 симв. и
|
||||
аудит-логом). Конкретные суммы задаёт владелец в админке — в документе НЕ выдумываем.
|
||||
Окно подтверждения цены (§3.2/§3.3) показывает актуальную цену из настроек.
|
||||
|
||||
### 5.5. Создание проектов и префлайт
|
||||
Создание самих проектов — **бесплатно** (как сейчас). «Создать и запустить» гоняет
|
||||
существующий `BalancePreflightService`; при нехватке — 409 `balance_insufficient` и
|
||||
существующий `ProjectLimitOverloadDialog` (переиспользуем из `NewProjectDialog.vue`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Объём и настройки агента (🟡)
|
||||
- Шаг 1: до **15** конкурентов на выдаче (настраиваемо: `autopodbor_max_competitors`).
|
||||
- Шаг 2: без жёсткого потолка источников, но с дедупом; разумный технический предел в
|
||||
реализации движка.
|
||||
- Эти числа — параметры запроса к агенту (§7.2), не зашиты в UI.
|
||||
|
||||
---
|
||||
|
||||
## 7. «Розетка» движка (контракт для будущего мозга)
|
||||
|
||||
### 7.1. Интерфейс
|
||||
```php
|
||||
interface CompetitorAgent
|
||||
{
|
||||
public function findCompetitors(FindCompetitorsRequest $r): FindCompetitorsResult; // шаг 1
|
||||
public function studyCompetitor(StudyCompetitorRequest $r): StudyCompetitorResult; // шаг 2
|
||||
public function resolveByName(ResolveByNameRequest $r): ResolveByNameResult; // manual по названию
|
||||
}
|
||||
```
|
||||
Привязка в контейнере: v1 → `FakeCompetitorAgent`, потом → `RealCompetitorAgent`
|
||||
(HTTP API + ключ). Джобы и сервисы зависят ТОЛЬКО от интерфейса — смена движка не трогает
|
||||
экраны, данные и деньги.
|
||||
|
||||
### 7.2. DTO (контракт вход/выход) — кратко
|
||||
- `FindCompetitorsRequest`: `region_code`, `examples[]` (сайт/справочник),
|
||||
`about_self{sites[],directories[],description}`, `include_federal`, `max_competitors`.
|
||||
- `FindCompetitorsResult`: `competitors[]{name, description, is_federal, relevance_pct,
|
||||
site_url, directory_urls[], provenance}`.
|
||||
- `StudyCompetitorRequest`: `competitor{name, site_url?, directory_urls[]?}`, `region_code`.
|
||||
- `StudyCompetitorResult`: `sources[]{signal_type, identifier, phone_kind?, provenance_url,
|
||||
provenance_label}`.
|
||||
- `ResolveByNameRequest`: `name`, `region_code`.
|
||||
- `ResolveByNameResult`: `candidates[]{name, description, site_url?, directory_urls[]?,
|
||||
provenance}` (0, 1 или несколько).
|
||||
|
||||
Нормализация (голова домена, телефон `7xxxxxxxxxx`, различение настоящий/подменный по
|
||||
методу из notes 6f) — на стороне backend (`AutopodborDedup`/нормалайзер), НЕ внутри
|
||||
движка, чтобы заглушка и реальный движок были взаимозаменяемы и единообразны.
|
||||
|
||||
### 7.3. Заглушка `FakeCompetitorAgent` (v1)
|
||||
Возвращает заготовленные данные (как в прототипе: Окна Комфорт 100%, Пластика Окон 96% и
|
||||
т.д.), с искусственной задержкой, чтобы прогнать весь поток, деньги, сохранение и UI
|
||||
по-настоящему. Под фиче-флагом/конфигом выбора реализации.
|
||||
|
||||
### 7.4. Доверие и риск «выдумал»
|
||||
Результаты — кандидаты. Защита: каждый конкурент и источник несёт провенанс-ссылку
|
||||
«где нашли»; клиент проверяет и сам выбирает; «уже есть проект» помечается; ничего не
|
||||
создаётся без явного выбора клиента. Никаких авто-действий от имени агента.
|
||||
|
||||
---
|
||||
|
||||
## 8. Швы (отвлёкся / закрыл / вернулся) — по notes 6b/6i/6j
|
||||
- Фоновый прогон не прерывается закрытием вкладки; возврат → «идёт…» или готовый результат.
|
||||
- История в `autopodbor_runs`/`competitors`/`sources` → «Продолжить начатое» (§3.1).
|
||||
- Поштучный прогресс: изученный конкурент открывается БЕЗ оплаты; неизученный — платно.
|
||||
- Живое состояние источников при каждом заходе (§4.4); «Изменить проект» прямо оттуда.
|
||||
- Уведомления — только в портале (значок/тост), без почты.
|
||||
|
||||
---
|
||||
|
||||
## 9. Хвосты реализации (трекать в плане)
|
||||
1. **Уникальность имени проекта** при пачке одинаковых имён → тихий числовой суффикс (§4.5).
|
||||
2. **Эмодзи поставщику** → strip перед `SupplierProjectDto.name`; проверить, принимает ли
|
||||
BG юникод (если принимает — можно не strip-ать). До проверки — strip по умолчанию.
|
||||
3. **Реальный движок** — отдельная сессия; вставляется в розетку §7 без изменений вокруг.
|
||||
4. **Настоящий hold-резерв в леджере** — отложен; вернуться при росте нагрузки (§5.2).
|
||||
5. **`manage-project.js` GAPS** (workdays/regions игнор) — существующее ограничение синка,
|
||||
фича его не усугубляет; учесть при ожиданиях «дни/регионы у поставщика».
|
||||
|
||||
---
|
||||
|
||||
## 10. Тестирование
|
||||
- **Backend (Pest):** RunService (идемпотентность — один in-flight, нет двойного debit);
|
||||
ChargeService (списание только при `done`, атомарность, новый тип проводки, money-инвариант);
|
||||
дедуп (конкуренты/источники/«уже есть проект»); нормализация домена/телефона; маппинг
|
||||
источник→ProjectService::create; суффикс уникального имени; strip эмодзи в DTO.
|
||||
Все тесты с записью через supplier-PDO — `uses(SharesSupplierPdo::class)` (memory-правило).
|
||||
- **Контракт движка:** `FakeCompetitorAgent` + тесты, что поток целиком работает на заглушке.
|
||||
- **RLS:** новые таблицы — `rls-reviewer` + проверки cross-tenant изоляции.
|
||||
- **Frontend (Vitest):** состояния `s-loading`/`empty`/`done`; блокировка «уже есть проект»;
|
||||
подтверждение по названию (🟡); подтверждение цены; префлайт-409 переиспользование.
|
||||
- **Деньги:** прогон billing-audit перед мержем.
|
||||
- **UI-smoke:** прокликать поток на заглушке (Playwright), сверить с прототипом.
|
||||
|
||||
---
|
||||
|
||||
## 11. Открытые вопросы к владельцу (помечены 🟡 в тексте)
|
||||
1. Подтверждение найденной по названию компании ДО оплаты (§3.3) — ок?
|
||||
2. Экран «ничего не нашли», деньги не списываем (§3.7) — ок?
|
||||
3. Источники только сайт+телефон, без SMS (§4.2) — ок?
|
||||
4. Объём шага 1 = до 15 конкурентов, настраиваемо (§6) — ок?
|
||||
5. Цена/вкл-выкл через админку, без выдуманных сумм (§5.4) — ок?
|
||||
|
||||
После «ок» по этим пятёрке и по документу в целом — перехожу к skill `writing-plans`
|
||||
(план реализации). Код до утверждённого плана не пишу.
|
||||
Reference in New Issue
Block a user