Files
portal/docs/superpowers/plans/2026-06-20-acceptance-PR2-projects-plan.md
T
Дмитрий dc14afdedd docs(приёмка): поправки планов по находкам ревью (Татарстан/капча/lpimp/воронка/№23/append-only)
Закрыты doc-находки двух ревью-сессий (точность планов перед прод-прогоном):
- GAP-1: Татарстан 16→19 (16=Мордовия, конституц. порядок ст.65, НЕ ГИБДД) —
  свод, план №3 (C-2), PR2 (сноска: P5 [16]=Мордовия, не Татарстан). Сверено
  по RussianRegions.php.
- M-2/GAP-3: капча = NullCaptchaVerifier БЕЗУСЛОВНО (не только local) — план №18.
- lpimp-status: lpimp_ → 401 на биллинг/api-keys (не 403), 403 только на admin,
  /api/billing/charges читается — план №25.
- N-6: воронка статусов НЕ форсится (любой валидный slug, нет state-machine) — план №13.
- №23-tx: импорт пишет 1 нулевую historical_import строку; сверять баланс/lead_charges,
  не число balance_transactions — план №23.
- append-only: lead_charges защищён GRANT'ом (prod/crm_app_user), не триггером →
  на dev-superuser правка пройдёт (ложный GREEN); гонять на проде — план №12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:38:37 +03:00

139 lines
15 KiB
Markdown

# Приёмка liderra.ru — ПУНКТ PR2: завести 16 проектов через портал → 12 rt у поставщика
> **Для исполнителя:** план в формате PR1 (одобрен владельцем 20.06). Каждый шаг = `🔧 Код-факт (file:line)` · `🎬 Действие` · `📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)` · `❌ Если не так`. Программистские детали остаются «под капотом»; владельцу в эфир идут только человеческие карточки.
**Цель:** показать, что клиент заводит свои проекты **через портал** (а не «из-под капота»), и портал сам передаёт их поставщику в правильном, сгруппированном виде — 16 проектов в кабинетах превращаются в **12 «живых» (rt) у поставщика**, лишнее не задваивается.
**Что снимаем глазами:** раздел **«Проекты»** в кабинете (16 карточек по 7 клиентам) — живые 📸. Экран администратора поставщика (12 rt) — 🟦 только на боевом прогоне (локально нет реального поставщика crm.bp-gr.ru).
**Источник сценария:** R1 §«Проекты (16)» (`docs/superpowers/runbooks/2026-06-18-acceptance-r1-provisioning.md`), карточка PR2.
---
## 🔧 Код-факты (подтверждено чтением 20.06)
- Создание проекта: **POST /api/projects** → [ProjectController::store :125-165](../../../app/app/Http/Controllers/Api/ProjectController.php#L125-L165).
- **Гейт реквизитов** — первый проект без реквизитов → `requisites_required` 422 — [:131-133](../../../app/app/Http/Controllers/Api/ProjectController.php#L131-L133). Значит **перед PR2 реквизиты у каждого клиента должны быть заполнены** (минимум `subject_type+contact_name+contact_phone`, +`inn` для ЮЛ/ИП — [RequisitesService.php:31-44](../../../app/app/Services/Requisites/RequisitesService.php#L31-L44)).
- **Префлайт баланса** при создании — если суммарный дневной лимит проектов не обеспечен балансом → `balance_insufficient` 409 — [:140-156](../../../app/app/Http/Controllers/Api/ProjectController.php#L140-L156). (16 проектов × лимит 10 = до 60 лидов/день на C1; при 100 000 ₽ и 500 ₽/лид запас 200 — проходит.)
- Поля запроса: `name, signal_type(site|call|sms), daily_limit_target(1..10000), regions[](present, каждый 1..89, []=вся РФ), delivery_days_mask(1..127)`; для `site``signal_identifier` домен; для `call``7XXXXXXXXXX`; для `sms``sms_senders[]` (+опц. `sms_keyword`) — [StoreProjectRequest.php:17-47](../../../app/app/Http/Requests/StoreProjectRequest.php#L17-L47).
- После создания online-режим шлёт **SyncSupplierProjectJob** → upsert `supplier_projects` + pivot `project_supplier_links` — [SyncSupplierProjectJob.php:29-56](../../../app/app/Jobs/SyncSupplierProjectJob.php#L29-L56).
- **Группировка (почему 16→12):** rt у поставщика = по `(платформа × уникальный ключ источника)` — [SupplierProjectGrouping::resolvePlatforms :51-64](../../../app/app/Services/Supplier/SupplierProjectGrouping.php#L51-L64):
- сайт/звонок → **B1+B2+B3** (3 rt на один ключ),
- смс+слово → **B2+B3** (2 rt),
- смс без слова → **B3** (1 rt),
- DIRECT → **0 rt** (поставке не передаётся).
Итог R1: корень-сайт 3 · субдомен 3 · звонок 3 · смс+слово 2 · смс 1 · DIRECT 0 = **12**. Несколько клиентов на одном источнике (P1..P7 на `test-okna.ru`) **схлопываются** в те же 3 rt — это и есть «лишнее не задваивается».
- Локальный засев клиентов/проектов: [ImitationSeedCommand :72-96](../../../app/app/Console/Commands/Imitation/ImitationSeedCommand.php#L72-L96) (factory, баланс 100 000 ₽, обход гейта). Засев вставляет pivot **напрямую** и НЕ ходит к реальному поставщику — поэтому «12 rt у поставщика» локально не показать.
---
## Популяция проектов (R1 §«Проекты (16)»)
| P# | Клиент | Источник | Тип | Регионы | Лимит | Дни |
|---|---|---|---|---|---|---|
| P1 | C1 | test-okna.ru | сайт | [82] | 10 | 127 |
| P2 | C2 | test-okna.ru | сайт | [82] | 10 | 127 |
| P3 | C3 | test-okna.ru | сайт | [83] | 10 | 127 |
| P4 | C4 | test-okna.ru | сайт | [] вся РФ | 10 | 127 |
| P5 | C5 | test-okna.ru | сайт | [16] | 10 | 127 |
| P6 | C6 | test-okna.ru | сайт | [82,83] | 10 | 127 |
| P7 | C7 | test-okna.ru | сайт | [56] | 10 | 127 |
| P8 | C1 | msk.test-okna.ru | сайт | [82] | 10 | 127 |
| P9 | C1 | 79990001122 | звонок | [82] | 10 | 127 |
| P10 | C2 | 79990001122 | звонок | [82] | 10 | 127 |
| P11 | C1 | TESTKW +слово «окна» | смс+слово | [82] | 10 | 127 |
| P12 | C2 | TESTKW +слово «окна» | смс+слово | [82] | 10 | 127 |
| P13 | C1 | TESTSMS | смс | [82] | 10 | 127 |
| P14 | C2 | TESTSMS | смс | [82] | 10 | 127 |
| P15 | C1 | test-direct.ru | DIRECT | [82] | 10 | 127 |
| P16 | C2 | test-direct.ru | DIRECT | [82] | 10 | 127 |
> **Коды регионов — порядковые 1..89 (конституционный порядок, НЕ ГИБДД):** [82]=Москва, [83]=СПб, [56]=Московская обл, [16]=**Мордовия** (P5 — это Мордовия, не Татарстан!), [19]=Татарстан. Сверено по `RussianRegions.php`.
**rt у поставщика (итог): 12** (корень-сайт 3 · субдомен 3 · звонок 3 · смс+слово 2 · смс 1 · DIRECT 0).
---
## Шаг PR2-0 — Предусловие: реквизиты заполнены (иначе гейт)
**🔧 Код:** [ProjectController::store :131-133](../../../app/app/Http/Controllers/Api/ProjectController.php#L131-L133).
**🎬 Действие (лок):** у каждого из 7 клиентов заполнить минимальные реквизиты (вкладка «Реквизиты»). На засеянных factory-клиентах гейт уже обойдён — шаг нужен, если клиенты заведены онбордингом.
**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Первый проект без реквизитов | — | портал не даёт создать: «нужны реквизиты» | _(слот прогона)_ | 📸 экран Проекты с подсказкой про реквизиты |
| После заполнения реквизитов | гейт стоит | гейт снят, проект можно создать | _(слот)_ | 📸 вкладка Реквизиты «Готово» |
**❌ Если не так:** проект создаётся без реквизитов → дефект гейта (пункт #19 ловит это отдельно).
---
## Шаг PR2-1 — Завести 16 проектов через портал
**🔧 Код:** [ProjectController::store :125-165](../../../app/app/Http/Controllers/Api/ProjectController.php#L125-L165); поля — [StoreProjectRequest :17-47](../../../app/app/Http/Requests/StoreProjectRequest.php#L17-L47).
**🎬 Действие (лок):** под каждым клиентом создать его проект(ы) по таблице P1–P16 — кнопкой «Новый проект» в разделе «Проекты» (или API `POST /api/projects` с полями из код-фактов). 16 раз.
**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Проектов у 7 клиентов | 0 (или только засеянный) | 16 проектов по таблице, тип/регионы/лимит верны | _(слот)_ | 📸 раздел «Проекты» каждого клиента (карточки) |
| Проектов в базе | — | 16 строк `projects` под тест-клиентами | _(слот)_ | 📝 SQL `SELECT count(*) FROM projects WHERE tenant_id IN (<TEST>)` = 16 |
**💡 Что внутри:** портал записал 16 проектов, по одному на строку таблицы; у каждого свой источник, регионы и дневной лимит. Это «карта», по которой портал будет принимать заявки.
**❌ Если не так:** проект не создаётся (422 реквизиты / 409 баланс) → вернуться к PR2-0 / поднять баланс (PR3); тип/регионы не те → ошибка ввода.
---
## Шаг PR2-2 — Проверить, что проекты видны и корректны в кабинете
**🔧 Код:** [ProjectController::index :39-122](../../../app/app/Http/Controllers/Api/ProjectController.php#L39-L122) (фильтры/сортировка/пагинация — раздел «Проекты»).
**🎬 Действие (лок):** открыть раздел «Проекты» под C1 (6 проектов) и под C2 (5 проектов) — глазами сверить источник/регионы/лимит с таблицей.
**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Карточки проектов C1 | пусто | 6 карточек (P1,P8,P9,P11,P13,P15), параметры верны | _(слот)_ | 📸 «Проекты» под C1 |
| Карточки проектов C2 | пусто | 5 карточек (P2,P10,P12,P14,P16) | _(слот)_ | 📸 «Проекты» под C2 |
| Чужие проекты | — | клиент видит только свои (изоляция) | _(слот)_ | 📸 (свой список без чужих) |
**❌ Если не так:** проект чужого клиента виден → утечка (пункт #16 ловит отдельно); параметры не совпали → ошибка создания.
---
## Шаг PR2-3 — Синхронизация к поставщику: 12 rt + pivot (🟦 на прод-прогоне)
**🔧 Код:** [SyncSupplierProjectJob :29-56](../../../app/app/Jobs/SyncSupplierProjectJob.php#L29-L56); группировка [SupplierProjectGrouping::resolvePlatforms :51-64](../../../app/app/Services/Supplier/SupplierProjectGrouping.php#L51-L64).
**🎬 Действие (прод):** после создания проектов через портал — открыть админку поставщика и сверить число rt-проектов; в базе проверить pivot.
**🎬 Действие (лок):** показать **нельзя** (нет реального crm.bp-gr.ru) — вместо экрана даём 💡-объяснение группировки по коду.
**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| rt у поставщика | — | **12 rt** (3+3+3+2+1+0 по группам) | _(слот боевого прогона)_ | 📸 админка поставщика (🟦 прод) |
| pivot `project_supplier_links` | пусто | заполнен для B-каналов | _(слот)_ | 📝 SQL `SELECT count(*) FROM project_supplier_links WHERE …` (внутр.) |
| Схлопывание дублей | — | 7 клиентов на `test-okna.ru` → те же 3 rt, не 21 | _(слот)_ | 💡 объяснение по коду группировки |
**💡 Что внутри:** хоть проектов 16, поставщику не нужно 16 отдельных «приёмников». Портал умно объединяет: все, кто ловит с одного источника, садятся на один и тот же приёмник у поставщика. Поэтому 16 кабинетных проектов = 12 реальных приёмников; ничего не задвоено, ничего лишнего поставщику не ушло. (DIRECT-проекты поставщику вообще не передаются — заявки по ним приходят другим путём.)
**❌ Если не так:** rt не появились → синк упал (логи) или режим не online; pivot пуст → инъекция B-канала не сматчится (ломает R2/R3).
---
## Verification PR2 (выход из пункта)
- [ ] 📸 Раздел «Проекты» под C1 (6) и C2 (5) — карточки с верными источником/регионами/лимитом.
- [ ] 📸 (или сводно) у остальных C3–C7 их проекты видны.
- [ ] 📝 SQL: `count(projects WHERE tenant_id IN TEST)` = 16.
- [ ] 🟦 на прод-прогоне: админка поставщика = **12 rt**; pivot заполнен.
- [ ] 📸 изоляция: клиент видит только свои проекты.
## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** раздел «Проекты» каждого клиента (16 карточек суммарно), вкладка «Реквизиты», (прод) админка поставщика.
- **📝 Текст:** число строк `projects`, заполнение pivot, лог синка — глазами в портале не видны.
## Грабли
- На local синк к реальному поставщику не идёт → «12 rt» только на прод-прогоне; локально доказываем 16 проектов + механику группировки.
- DIRECT (`test-direct.ru`, P15/P16) — у поставщика **0 rt** (по дизайну); заявки по нему вливаются DIRECT-каналом, не B-каналом.
- Префлайт баланса 409 при больших лимитах — для PR2 лимиты 10, запас есть; если поднимали лимиты под R4, заводить проекты ДО подъёма или с балансом ≥ требуемого.