docs(автоподбор): записка для продолжения после компакта (state + что прочитать)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-28 13:54:44 +03:00
parent 036fffbefe
commit bb3dbe76e8
@@ -0,0 +1,117 @@
# Автоподбор конкурентов — ЗАПИСКА ДЛЯ ПРОДОЛЖЕНИЯ (после компакта)
> Дата: 2026-06-28. Это handoff-файл: прочитай его ПЕРВЫМ, затем по списку §«Что прочитать».
> Цель — продолжить реализацию фичи «Автоподбор конкурентов» с того места, где остановились.
## 0. Что прочитать, чтобы восстановить контекст (в этом порядке)
1. Этот файл целиком.
2. План реализации (главный источник задач): `docs/superpowers/plans/2026-06-28-avtopodbor-konkurentov.md`
— в нём блок **КОНВЕНЦИИ** (обязательны) + 29 задач. ВНИМАНИЕ: код-блоки в плане для
некоторых задач писались против устаревшей базы; ниже §4 «Поправки» перечисляет, что
реально изменилось. Доверяй §4 и реальному коду, а не дословно плану.
3. Дизайн-документ (что и зачем): `docs/superpowers/specs/2026-06-28-autopodbor-konkurentov-design.md`
4. Прототип UX (экраны фронта 1:1): `docs/superpowers/specs/2026-06-28-autopodbor-konkurentov-prototype.html`
5. Заметки брейншторма (решения владельца): `docs/superpowers/specs/2026-06-28-autopodbor-konkurentov-brainstorm-notes.md`
6. Один готовый бэкенд-пример для стиля джоб: `app/app/Jobs/Autopodbor/RunAutopodborStudyJob.php`
## 1. Где работаем (ОКРУЖЕНИЕ — критично)
- **Worktree:** `C:/моя/проекты/портал crm/Документация/.claude/worktrees/avtopodbor` (изолированный).
- **Ветка:** `worktree-avtopodbor`. РАБОТАТЬ ТОЛЬКО В НЕЙ. Без checkout/switch/merge/rebase.
- **База:** ветка стоит поверх `gitea/main` (реальный main!). `origin` = github = МЁРТВОЕ зеркало,
НЕ использовать как базу. Если надо обновить базу — `gitea/main`.
- **Окружение готово:** vendor под gitea установлен (composer install сделан), `.env` скопирован,
`storage/framework/*` созданы. Переустанавливать НЕ надо.
- **Тестовая БД = `liderra_testing_apk`** (ВЫДЕЛЕННАЯ, собрана из gitea v8.57 + все миграции вкл. наши).
- тесты: `cd app && DB_DATABASE=liderra_testing_apk ./vendor/bin/pest <путь>`
- миграции на тест: `cd app && DB_DATABASE=liderra_testing_apk php artisan migrate --force`
- Боевую `liderra` и общую `liderra_testing` НЕ ТРОГАТЬ.
- **Машина слабая:** мало RAM/диска, git worktree-создание ОOM-ило (решилось закрытием окон).
Тяжёлые операции (npm install) — осторожно, возможен OOM.
- **lefthook не в PATH** в worktree — pre-commit хуки не срабатывают, коммиты проходят (это норм здесь).
## 2. Протокол работы (как продолжать)
- Метод: **subagent-driven** — на каждую задачу свежий субагент (Agent tool, general-purpose),
**модель sonnet** (задачи коммитят → §15.1 запрещает Haiku для git-задач).
- **Escape на коммиты ПОЛУЧЕН** от владельца (коммиты в рамках фичи; НЕ пушить, НЕ выкатывать).
- TDD: тест→fail→код→pass→коммит (явные пути, без `git add -A`, без пайпов в commit).
- Каждому субагенту в конце требовать блок `=== GIT REPORT ===` (cwd/branch/HEAD/HEAD^/status).
- После субагента — §B пост-проверка: HEAD сместился на 1 коммит, parent = pre-spawn HEAD,
branch=worktree-avtopodbor, файлы в scope.
- Коммит-сообщения на русском + `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
- Темп: владелец сказал **«до конца без остановок»** — продолжать автономно бэкенд→фронт→финал.
## 3. Состояние: СДЕЛАНО (закоммичено, HEAD=78d19654 на момент записки)
Фундамент: rebase на gitea/main; RLS-харднинг v8.57; выделенная БД liderra_testing_apk.
Готовые задачи (все тесты зелёные на liderra_testing_apk):
- **Task 1-3**: миграции `autopodbor_runs`/`autopodbor_competitors`/`autopodbor_sources` + RLS (CHANGELOG v8.58).
- **Task 4**: модели `AutopodborRun`/`AutopodborCompetitor`/`AutopodborSource`.
- **Task 5**: `BalanceTransaction::TYPE_AUTOPODBOR_CHARGE='autopodbor_charge'` + сид-миграция 4 ключей `system_settings`
(`autopodbor_enabled`=0, `autopodbor_price_search_rub`=0, `autopodbor_price_study_rub`=0, `autopodbor_max_competitors`=15)
+ миграция расширения CHECK-констрейнта `balance_transactions_type_check`.
- **Task 6**: 6 DTO в `app/app/Services/Autopodbor/Agent/Dto/`.
- **Task 7**: интерфейс `CompetitorAgent` + `FakeCompetitorAgent` + binding в `app/bootstrap/providers.php`
(`AutopodborServiceProvider`). NB: в `app/tests/Pest.php` добавлена строка
`pest()->extend(TestCase::class)->in('Unit/Autopodbor')` (нужно для unit-тестов с контейнером).
- **Task 8**: `AutopodborNormalizer` (domainHead/phone/sourceKey/competitorKey).
- **Task 9**: `AutopodborDedup` (existingProjectId/dedupSources/dedupCompetitors).
- **Task 10**: `AutopodborChargeService::chargeForRun` (идемпотентно по `balance_transaction_id`, bcmath,
только при успехе; bcmath проверено вручную; полный billing-audit — на финале Task 29).
- **Task 12**: `RunAutopodborSearchJob` (шаг 1: подбор → дедуп → сохранить → списать → done; пусто→empty без списания).
- **Task 13**: `RunAutopodborStudyJob` (шаг 2: изучение → источники нормализованные → списать → конкурент studied → done).
## 4. ПОПРАВКИ к плану (узнано из реального кода — соблюдать!)
- RLS-GUC = **`app.current_tenant_id`** (НЕ `app.tenant_id`), форма ХАРДНИНГ:
`USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::bigint)`.
- Tenant-контекст в ДЖОБАХ: в начале `handle()``DB::statement("SET app.current_tenant_id = ".$run->tenant_id)`
(СЕССИОННО, не LOCAL — чтобы видели и status-update, и вложенная транзакция charge). Класса `TenantContext::set` НЕТ.
- Tenant-контекст в ТЕСТАХ с прямыми моделями: `DB::statement("SET LOCAL app.current_tenant_id = ".$tenant->id)`
после `Tenant::factory()->create()`, плюс `uses(DatabaseTransactions::class, \Tests\Concerns\SharesSupplierPdo::class)`.
HTTP-тесты — `$this->actingAs($user)->...` (middleware сам ставит GUC). `User::factory()->create(['tenant_id'=>$tenant->id])`.
- `App\Support\PhoneNormalizer::normalize()` возвращает **`+7XXXXXXXXXX`** → в нормализаторе срезаем `+` (получаем `7xxxxxxxxxx`).
- `App\Exceptions\Billing\InsufficientBalanceException` — конструктор `(int $priceKopecks, string $balanceRub)` (НЕ без аргументов).
- `BalanceTransaction`: `$timestamps=false` → нужен явный `created_at`; поля `amount_leads`/`balance_leads_after` = null (как в LedgerService).
- `BalanceTransaction` типы ограничены CHECK-констрейнтом `balance_transactions_type_check` — новый тип уже добавлен миграцией Task 10.
- `SystemSettings::get()/bool()` — СТАТИЧЕСКИЕ методы. Сид настроек — через `SystemSetting::updateOrCreate(['key'=>..],['value'=>..,'type'=>..])`.
- RefreshDatabase глобально ВЫКЛЮЧЕН (Pest.php). Для записи в БД — `uses(DatabaseTransactions::class)` (+SharesSupplierPdo если пишешь supplier-flow).
- Образец ledger: `app/app/Services/Billing/LedgerService.php`; образец money-теста: `tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php`.
## 5. ОСТАЛОСЬ — порядок (ВАЖЕН reorder джоб перед RunService)
Бэкенд:
- **Task 14**: `RunAutopodborResolveJob` (резолв по названию → конкуренты origin='resolve', search_run_id=run.id, БЕЗ списания; пусто→empty).
Мирроль SearchJob, но `agent->resolveByName(ResolveByNameRequest(name,region))`, сохранять кандидатов как competitors, chargeForRun НЕ звать.
- **Task 11**: `AutopodborRunService` (`startSearch/startStudy/startResolve`) + исключение `App\Exceptions\Autopodbor\RunInFlightException`.
Гейт баланса перед стартом (SystemSettings::get цены vs tenants.balance_rub, bccomp); один in-flight на (tenant,kind);
диспатч соответствующей джобы. startStudy: если competitor.studied_at!=null → вернуть его studyRun без оплаты.
startResolve — без гейта (бесплатно). Тест с `Queue::fake()` + `Queue::assertPushed(...)` (классы джоб ТЕПЕРЬ существуют).
- **Task 15**: `AutopodborProjectCreator::createFromSources($tenantId,$sourceIds,$common,$launch)` — переиспользует
`App\Services\Project\ProjectService::create`. Имя проекта = имя конкурента + значок (call+real→' ✓', call+substitute→' 🎭', site→без значка);
коллизия имени (`assertNameUnique` бросает 422) → тихий числовой суффикс ' 2',' 3'. Привязать `source.created_project_id`.
СВЕРИТЬ сигнатуру `ProjectService::create(Tenant $tenant, array $data)` и трактовку `force_save_blocked` по реальному коду.
- **Task 16**: `SupplierProjectDto::stripBadge(string): string` — срезать хвостовой значок/эмодзи; применить там, где `dto.name` собирается для поставщика (синк). СВЕРИТЬ, где формируется name для `app/playwright/manage-project.js`.
- **Task 17**: `AutopodborController` + роуты `routes/api.php` (под клиентским auth-middleware, рядом с projects) + Resources.
Эндпоинты: GET `autopodbor/state` (enabled+runs+prices), GET `runs/{run}`, GET `competitors/{competitor}` (источники + живое `existing_project_id` через AutopodborDedup),
POST `search`/`study`/`resolve`/`manual-study`/`sources/manual`/`projects`. Мапить RunInFlightException→409, InsufficientBalanceException→409.
Образец API-теста: `tests/Feature/Plan5/Projects/ProjectsStoreTest.php` (actingAs+postJson).
Фронтенд (ПЕРЕД ним: `cd app && npm install` — ТЯЖЕЛО, возможен OOM; если упадёт — сообщить владельцу):
- **Task 18**: `app/resources/js/api/autopodbor.ts` + `app/resources/js/stores/autopodborStore.ts` (+ vitest).
- **Task 19**: `AutopodborView.vue` + маршрут роутера + пункт меню «Автоподбор конкурентов» NEW (группа «Работа», видимость=enabled).
- **Task 20-26**: экраны Vue 1:1 по прототипу (Entry+«Продолжить начатое», AutoForm, ManualForm+ResolveConfirm,
Loading/Empty/Done, CompetitorList, SourceList+живое состояние+«Изменить проект», CreateProjects). Стиль Forest.
Переиспользовать `ProjectLimitOverloadDialog` для 409 при «Создать и запустить»; `NewProjectDialog.vue` (mode=edit) для «Изменить проект».
Финал:
- **Task 27**: проверить/добавить ключи `autopodbor_*` в админ-UI настроек (`SystemSettingEditDialog.vue` / `AdminSystemSettingsController`).
- **Task 28**: сквозной UI-smoke на заглушке (Playwright), скриншоты (НЕ коммитить *-LOCAL).
- **Task 29**: `/regression full`; `rls-reviewer` по 3 таблицам; **billing-audit** по `AutopodborChargeService`;
синхронизировать `db/schema.sql` (header-метрики + CREATE TABLE 3 таблиц) с CHANGELOG v8.58; self-review; память; finishing-a-development-branch (после «эскейп»).
## 6. Хвосты/риски
- `db/schema.sql` (канон-дамп) ещё НЕ синхронизирован с 3 новыми таблицами — сделать на Task 29 (тест-БД строится из миграций, поэтому не блокирует, но канон должен сойтись; счётчик RLS-политик 44→47).
- Сид-миграция настроек и CHECK-констрейнт-миграция применены к liderra_testing_apk; на боевую НЕ катить (это отдельный выкат потом).
- Движок — заглушка `FakeCompetitorAgent`; реальный ИИ-движок делает отдельная сессия, подключается заменой binding в `AutopodborServiceProvider`.
- npm для фронта ещё НЕ ставился.
## 7. Первое действие после компакта
1. Прочитать §0 список.
2. `git -C "<worktree>" log --oneline gitea/main..HEAD` — убедиться HEAD=78d19654 (или новее) и дерево чистое.
3. Pre-spawn §A (branch/HEAD), затем дispatch субагента на **Task 14** (RunAutopodborResolveJob) по §5.