diff --git a/docs/superpowers/specs/2026-06-28-autopodbor-konkurentov-RESUME.md b/docs/superpowers/specs/2026-06-28-autopodbor-konkurentov-RESUME.md new file mode 100644 index 00000000..5c98cf71 --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-autopodbor-konkurentov-RESUME.md @@ -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) `. +- Темп: владелец сказал **«до конца без остановок»** — продолжать автономно бэкенд→фронт→финал. + +## 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 "" log --oneline gitea/main..HEAD` — убедиться HEAD=78d19654 (или новее) и дерево чистое. +3. Pre-spawn §A (branch/HEAD), затем дispatch субагента на **Task 14** (RunAutopodborResolveJob) по §5.