# Независимый критический разбор подготовки приёмочного теста liderra.ru

**Дата:** 21.06.2026. **Автор:** свежая сессия-ревьюер (независимый взгляд). **Кодовая фраза стены:** «роутер-наставник».
**Метод:** read-only. Реальный код `app/` читан напрямую (Read/Grep) + 5 суб-агентов адверсариально по доменам (деньги, изоляция, распределение, честность находок, новые фичи). Falsify, не confirm. Прод не трогался, код не правился.
**Что разбиралось:** мастер-хэндофф `2026-06-20-acceptance-MASTER-execution-handoff.md`, 26 пер-пунктовых планов `2026-06-20-acceptance-*`, реестр находок, свод проверок (imitation-checks-table), карта логики прода §0–§21, ранбуки R0–R5.

---

## 🧭 Краткий вердикт

**Готово к прод-прогону — УСЛОВНО (нужны доделки до старта).**

Ядро подготовки крепкое: код-факты в денежных и изоляционных планах **в массе верны** (проверено построчно), инварианты §18–§21 покрыты, движок (раздача→списание→сделка→изоляция) описан корректно. Деньги и изоляция — без найденных дефектов корректности в самом коде.

**НО** перед прогоном нужно закрыть 3 блокера исполнимости/точности и дополнить реестр 5 находками, которые подготовка пропустила (2 из них — security уровня HIGH, не отражены в реестре). Они **не ломают** money/isolation-проверки, но меняют картину «что продаём» и могут дать **ложный провал** на прогоне.

| Ось | Вердикт | Суть |
|---|---|---|
| 1. Точность код-фактов | 🟡 PASS с 1 жёстким GAP | file:line/цифры верны; **Татарстан=16 — ошибка (это 19; 16=Мордовия)** |
| 2. Полнота покрытия | 🟡 PASS с тонкими местами | инварианты покрыты; вечерняя заморозка §20 и «10 регионов без гео» §18.3 — тонко |
| 3. Честность находок | 🟠 GAP | 4/5 находок подтверждены; F-REMIND переоценена; **2 HIGH-находки пропущены** |
| 4. Исполнимость хэндоффа | 🔴 GAP | **корпус ранбуков untracked** (битые ссылки); капча-Null ломает тест онбординга №18 |
| 5. Безопасность прода | 🟢 PASS | рамки прописаны жёстко; риск — в самих пропущенных security-находках, не в прогоне |

---

## Ось 1 — Точность код-фактов (выборка перепроверена по коду)

**Метод:** взяты денежные планы №7/№8/№9/№10/№11/№12, изоляция №16, распределение №1–№6, новые фичи №18/№19/№24/№26 — каждое file:line открыто в `app/`.

### PASS (подтверждено построчно)
- **№7 тройная запись** — `LedgerService::chargeForDelivery` ([LedgerService.php:47-128](../../../app/app/Services/Billing/LedgerService.php#L47)): 1 UPDATE баланса (bcmath, :80-84) + INSERT `lead_charges` (:89-98) + INSERT `balance_transactions` (:100-110) + INSERT `supplier_lead_costs` (:117-123). Атомарно — обёрнуто `DB::transaction` в `RouteSupplierLeadJob::createDealCopyForProject`. ✔
- **№8 тариф-ступени** — сидер [PricingTierSeeder.php:18-25](../../../app/database/seeders/PricingTierSeeder.php#L18): T1 100×50000коп(500₽)…T7 NULL×25000(250₽) — **ровно** как в плане. Выбор ступени `resolveForCount(tiers, delivered_in_month+1)`, 1-based кумулятивно — **off-by-one нет**. ✔ (но это DB-seeded; на проде сверять с живой `pricing_tiers` — планы это честно пишут).
- **№9 откат+автопауза** — исключение (`:73-77`) → откат транзакции → автопауза `is_active=false` через `pgsql_supplier`/BYPASSRLS + `ZeroBalancePausedMail` rate 1/час через Redis `Cache::add` (SETNX). ✔
- **№10 два проекта = одно списание** — `DISTINCT ON (snap.tenant_id)` ([LeadRouter.php:137](../../../app/app/Services/Lead/LeadRouter.php#L137)) + второй слой `supplier_lead_deliveries insertOrIgnore`. ✔
- **№16 изоляция** — RLS на 41 tenant-таблице (`tenant_id = current_setting('app.current_tenant_id')`), приложение = `crm_app_user` (без BYPASSRLS) → RLS реально форсится; + явный `where(tenant_id)` defense-in-depth во всех клиентских контроллерах; signed-URL отчётов защищён HMAC. ✔
- **№18/№19/№24/№26** — самозапись (300₽ старт, 5 попыток, TTL 15м), гейт реквизитов, G7-A поддержка, G6 публичный API (`lpkapi_`, cursor 1–500, scope read, last_used_at) — **все 15+ код-фактов G6 подтверждены**; ни одной несуществующей фичи. ✔

### 🔴 GAP-1 — Татарстан = 16 НЕВЕРНО (это 19; 16 = Республика Мордовия)
- **Где в коде:** [RussianRegions.php:36](../../../app/app/Support/RussianRegions.php#L36) `16 => 'Республика Мордовия'`, [:39](../../../app/app/Support/RussianRegions.php#L39) `19 => 'Республика Татарстан'`. Нумерация — **конституционный порядок** (ст. 65), НЕ ГИБДД-коды (где 16 действительно Татарстан — автор спутал две системы).
- **Где в подготовке:** свод [imitation-checks-table.md:24](2026-06-20-acceptance-imitation-checks-table.md) «Татарстан=16»; план №3 (region-cascade) строка с регионами + сценарий C-2; вероятно PR2 (P5 «C5 [16]»).
- **Эффект на прогон:** исполнитель, засеяв «Татарстан» как `[16]`, реально целит в **Мордовию**; скриншот «Город: Татарстан» покажет «Республика Мордовия» → **ложный провал** каскада (или ложный успех). Москва=82/СПб=83/Москобл=56 в тех же планах — верны, ошибка только в Татарстане.
- **Фикс:** заменить Татарстан на **19** в плане №3, своде и PR2. Перепроверить намерение P5.

### Мелкие неточности (не блокеры)
- Line-drift ±1–3 строки в нескольких планах (№9 цитирует `:122` вместо `:123` — докблок; №4 `:303-316` вместо фактического сравнения `:327-339`). Поведение верно.
- **№12 цитирует `db/schema.sql:81-86` как опору для «6 audit-таблиц», но сам header там говорит «5»** — устаревший комментарий шапки схемы (предшествует v8.31, добавившей `tenant_operations_log`). Число «6» верно против фактических триггеров (`:3304-3344`), но цитата опровергает собственное (верное) число. Заодно — нарушение CLAUDE.md §8 (метрики схемы — канон в header): header рассинхронён (5 vs 6).
- **№23 (CSV-импорт): шаг «balance_transactions не выросло» — фактически неверен.** Импорт пишет одну нулевую строку `historical_import` в `balance_transactions` на прогон ([HistoricalImportService.php:255](../../../app/app/Services/Import/HistoricalImportService.php#L255)). Денежный инвариант цел (`lead_charges` и `balance_rub` не трогаются) — но проверочный шаг сформулирован ошибочно: сверять надо `lead_charges`/`balance_rub`, не число строк транзакций.

---

## Ось 2 — Полнота покрытия (планы × свод × инварианты §18–§21)

**PASS по основному:** 26 планов покрывают 26 типов свода; инварианты §18 (заказ/каскад/жребий), §19 (распределение лида построчно), §20 (заморозка), §21 (share-aware блок) — отражены в планах №1–№10. CAP=3, DISTINCT ON, 3-фазный каскад, взвешенный CSPRNG-жребий, vid+delivery двойная идемпотентность — все с шагами.

**Тонкие места (рекомендую усилить шагами, не критично):**
1. **Вечерняя заморозка §20.2 — под-покрыта.** Свод №9 — это **мгновенная** пауза в доставке (§19.5, баланс<цены). А **вечерний sweep @18:00** (`BalancePreflightSweepJob`): «смотрит на ЗАВТРА» (тенант мёрзнет с положительным балансом), и **разморозка сохраняет ручные паузы** (`paused_at < frozenAtWas`) — у этого инварианта **нет отдельного шага** ни в одном плане. Это денежно-видимое поведение (клиент может замёрзнуть «непонятно почему»). Рекомендация: добавить карточку «вечерняя заморозка по завтрашней ёмкости + разморозка не трогает ручные паузы» (гонится бэкдейтом `frozen_by_balance_at`).
2. **Share-aware `required` §21.2 — формула не вынесена в проверку.** Ожидаемый freeze/preflight-блок надо считать через долю `share = ⌈groupOrder × (лимит/Σ)⌉`, а не сырой лимит. Планы блокировки это упоминают, но без явного числового примера расчёта доли исполнитель легко посчитает «по лимиту» и получит ложное расхождение.
3. **§18.3 «10 субъектов без гео-фильтра у поставщика» (Москобл=56 и др.)** — упомянуто в своде, но отдельной проверки заказа «единственный непокрытый регион → уходит вся РФ» в PR2 нет. Минор (проверяется чтением таблицы `LIDERRA_TO_SUPPLIER`).
4. **Покрытие находок:** реестр не включает 2 HIGH-находки (см. Ось 3 M-1, M-2) — это пробел полноты самого реестра.

---

## Ось 3 — Честность находок (реестр перепроверен + адверсариальный поиск)

### Подтверждение заявленных находок
| Находка | Вердикт | Доказательство |
|---|---|---|
| **F-CSV** 🔴 CSV-инъекция | ✅ **ПОДТВЕРЖДЕНА и ШИРЕ заявленного** | 3 пути из реестра верны + **ещё 2 пропущены**: `CsvFormatter.php:31-38` (экранирует только `;"\n`, не `=+-@\t`) и `XlsxFormatter.php:29,40` (`setCellValue` сырой). Затронуты 4 типа отчётов × 2 формата. XLSX опаснее (Excel вычисляет формулы без предупреждения). |
| **F-DELPROJ** 🟡 | ✅ ПОДТВЕРЖДЕНА | [ProjectService.php:186-191](../../../app/app/Services/Project/ProjectService.php#L186) — 422 «есть сделки, поставьте на паузу». |
| **F-PDF** 🟡 | ✅ ПОДТВЕРЖДЕНА статически (можно снять «не подтверждено») | `PdfStubFormatter.php:18` throw → ловится `GenerateReportJob.php:100-106` → `markFailed`. Мягкий `failed` доказан кодом. |
| **F-REMIND** 🧹 | ⚠️ **ПЕРЕОЦЕНЕНА** | `'reminder'` в `auth.ts:11` (реестр: :12) и коммент `DealDetailBody.vue:3` — реальны. Но **«мёртвого тумблера» НЕТ**: `NotificationsTab.vue:31-46` массив `EVENTS` не содержит `reminder` → переключатель не рендерится. Это чистый мёртвый член type-union + коммент, не UI-дефект. (+ 2 косметических хвоста реестр пропустил: `AppLayout.vue:47`, прозаический коммент в schema.sql.) |
| **F-DEPTRAC** 🧱 | ✅ ПОДТВЕРЖДЕНА | [ImpersonationContext.php:7,36-37](../../../app/app/Http/Middleware/ImpersonationContext.php#L37) — Middleware→Mail. Валит php-коммиты. |
| **F-CORPUS** 🧱 | ✅ ПОДТВЕРЖДЕНА (см. Ось 4) | runbooks R0–R5 + prod-logic-map — untracked. |
| **F-DIGEST** | ✅ корректно снята | самоотвод обоснован (канон=схема, не докблок). |

### 🔴 Пропущенные находки (адверсариальный поиск — НЕТ в реестре)

**M-1 (HIGH) — Админ-зона fail-open, держится только nginx Basic Auth.**
[EnsureSaasAdmin.php:35-46](../../../app/app/Http/Middleware/EnsureSaasAdmin.php#L35): `handle()` **никого не аутентифицирует** — только `abort(403)`, если активен impersonation-маркер/`lpimp_`; иначе `return $next()`. Прежний fail-closed 503 **снят осознанно** (`:19-22`, стопгэп 25.05), вся защита делегирована nginx-паролю `.htpasswd-admin`. Под `/api/admin/*` без app-аутентификации: правка баланса любого тенанта (`updateBalance`, admin_id = стаб `system-balance@liderra.local`), refund/тариф, перезапись `supplier_webhook_secret`, 152-ФЗ-erasure, старт impersonation в любой тенант. `init` impersonation — **без rate-limit**.
**Эксплуатируемость:** целиком зависит от nginx. Любой прямой доступ к Laravel (открытый порт контейнера, внутр. сеть, дыра в location-regex) = полная компрометация без defense-in-depth.
**NB:** это **перекликается с F-T2** из доменного аудита 15.06 (memory `project-tails-audit-stage2b`) — т.е. находка **уже была известна ранее**, но в приёмочный реестр не попала. Рекомендация: внести в реестр как HIGH (выше F-CSV по серьёзности); восстановить app-слойный fail-closed гейт, чтобы nginx был defense-in-depth, а не единственным замком; проверить, что прод-nginx реально покрывает все варианты пути `/api/admin*`.

**M-2 — ✅ СДЕЛАНО (проверено по проду 22.06.2026): реальный Yandex SmartCaptcha включён.** На боевом `CAPTCHA_DRIVER=yandex` → `YandexSmartCaptchaVerifier` (HTTP-валидация у Yandex, пустой токен → отказ), серверный ключ в прод-`.env` задан, фронт-виджет в сборке `RegisterView-*.js`. Оценка ниже относилась к коду на 19.06 (NullCaptchaVerifier) — **устарела**.

~~**M-2 (HIGH/CRITICAL) — Капча самозаписи эффективно отключена.**~~ (исторически, на 19.06)
[AppServiceProvider.php:54-57](../../../app/app/Providers/AppServiceProvider.php#L54): `CaptchaVerifier` **безусловно** биндится на `NullCaptchaVerifier` (коммент: «SP1 — Null-драйвер; реальный Yandex SmartCaptcha встанет позже»). `NullCaptchaVerifier` пропускает любой непустой токен при `services.captcha.fake_passes` (default `true`, `services.php:42`). Реального драйвера в коде **нет**. → На проде любой непустой `captcha_token` проходит капчу; а `CAPTCHA_FAKE_PASSES=false` заблокировал бы и реальных юзеров (драйвера-то нет). С учётом старта **300₽** на подтверждение — вектор фарма (ограничен лишь нуждой в подтверждаемом email + throttle регистрации).
**Эффект на прогон:** план №18 утверждает «капча на проде реальная → массу аккаунтов руками не завести» — **STALE/желаемое**. Тест капчи/5-попыток как написан невалиден.
**Рекомендация:** прогон обязан проверить **фактический прод-`.env`** капчи (вдруг драйвер дописан после 19.06; в коде HEAD — нет). План №18 — поправить формулировку.

### 🟡 Прочие пропущенные (MEDIUM/LOW, подтверждены кодом)
- **Append-only только у `balance_transactions`.** «Тройная запись» №7 подаётся как несгораемая бухгалтерия, но **`lead_charges` (выручка) и `supplier_lead_costs` (себестоимость) НЕ имеют trigger-защиты** `audit_block_mutation` ([schema.sql:1143-1181](../../../db/schema.sql#L1143), 2589-2613). У `lead_charges` единственная защита — `GRANT` без UPDATE/DELETE, и он **закомментирован** (`:1180-1181`), применяется только на проде через `db/02_grants.sql` под `crm_app_user`. **На dev/test (postgres-superuser) строки `lead_charges` свободно правятся** → шаг №12-2 «правку списания не дают» даст **ложный GREEN** на локалке. Рекомендация: добавить в план №12 явную оговорку «гонять под `crm_app_user` на проде; dev-superuser НЕ заблокирует».
- **`tenants` без RLS-политики.** Латентный footgun: сегодня не течёт (нет клиентского unscoped-запроса к `Tenant`), но утверждение №16 «RLS фильтрует каждую выборку» для `tenants` технически ложно.
- **`lpimp_` → 401, не 403** на billing/api-keys (роуты несут только `auth:sanctum`, гарда `impersonation` там нет → ключ просто неаутентифицирован). План №25-4 предсказывает 403. Net-эффект (доступа нет) верен, код статуса — нет. **Плюс:** `/api/billing/charges` (`web.php:193`) **включает** `impersonation` → `lpimp_` **может читать ленту списаний** (read-only). Осознать на прогоне.
- **`/api/v1/deals` без rate-limit** прогоняет bcrypt на каждый кандидат-префикс на запрос — brute-force/DoS-поверхность (план №26 упоминает мягко).
- **`support_requests` RLS без `WITH CHECK`** (`schema.sql:847`) — write-side целостность tenant_id держится на app-коде, не на БД.

### Что активно прощупано и оказалось защищённым (НЕ находки — для уверенности)
Входящий вебхук (`hash_equals` + HMAC + rate 600/мин + IP-allowlist fail-closed на проде + vid-идемпотентность); исходящий SSRF-гард (резолв A/AAAA, блок private/reserved + IP-pinning от DNS-rebind) — **реальны и сильны**; хранение секретов (bcrypt, показ один раз); 2FA/сброс-пароля/брут кода impersonation (5 попыток, TTL); mass-assignment в DealController (только `$validated`, `tenant_id` сервер-сайд); SQLi в `bulkUpdateDays` (int-cast — safe); CSV-upload (10MB, mimes, UUID-имя — без traversal).

---

## Ось 4 — Исполнимость хэндоффа (сможет ли свежая сессия поднять прогон)

**🔴 GAP-2 — Корпус приёмки UNTRACKED → битые ссылки (F-CORPUS, подтверждено git):**
`git ls-files` не видит ни одного из: R0–R5, R3b, `prod-logic-map.md`, хартия, `stepbystep-plans.md`. Все — `??` (untracked). А мастер-хэндофф закоммичен (`b7e3af4c`) и **ссылается на них**. **Свежая сессия в чистом клоне их не найдёт — маршрут §2 (map→хартия→R0) рвётся на первом шаге.** Рекомендация: закоммитить корпус (docs-only — deptrac не мешает) ДО запуска новой сессии, либо запускать в текущей рабочей копии.

**🟠 GAP-3 — Капча-Null ломает тест онбординга №18 как написан** (см. M-2). Исполнитель не сможет проверить «реальную капчу» — её нет. Нужно сверить прод-`.env` и переформулировать шаг.

**🟡 Состояние стены неоднозначно.** Мастер §1 говорит «на приёмке стена снята — действуй свободно», но GUIDE описывает стену как **замороженно-активную** в этом репозитории, а провижининг требует прод-мутаций (SQL + owner-escape). Если стена реально поднята, исполнитель упрётся в неё на каждой мутации. Рекомендация: первым шагом сессии — одна проба состояния стены (`node --version`), и держать наготове механизм escape (GUIDE §escape) либо терминал владельца.

**Прочее (исполнимо, но требует владельца — хэндофф это честно отмечает):** секреты (вебхук, пароли) у владельца; `imitation:seed` на проде запрещён → тест-тенанты только SQL+escape; проекты — только через портал (синк); живые ящики kdv1@/stels_info@. Это всё проговорено в §6/§1 — провалов нет.

**Ограничение моего разбора:** ранбуки R0/R1/R5 построчно я НЕ читал (доступность подтверждена, содержимое — нет), оценка исполнимости — по мастеру + своду. Глубокую сверку шагов teardown/провижининга оставляю прогону.

---

## Ось 5 — Безопасность прода

**🟢 PASS.** Рамки прописаны жёстко и неотменяемо: tenant 2 `lkomega` не трогать (§1, ~1.83М₽/1013 сделок); rt у поставщика чистить ДО 21:00 МСК; маркер `TEST-` для teardown; секреты не в чат; критдефект денег/изоляции → СТОП по пункту + владельцу, не чинить на боевом; нагрузка R4 со SAFETY-стопом на слабом железе (2 ГБ рядом с lkomega). Порядок teardown в R5 строгий. Замечаний к самим рамкам нет.

**Нюанс (не дефект рамок, а контекст):** сам прогон активно использует админ-эндпоинты (PR3 `updateBalance` для балансов) — которые из-за **M-1** работают **без app-аутентификации**. Это и есть штатный канал провижининга, но владельцу важно понимать: тот же fail-open означает, что админ-операции теста ничем (кроме дисциплины исполнителя и nginx) не отделены от боевых — повышенная аккуратность около tenant 2.

---

## Сводная таблица находок ревью

| ID | Ось | Важность | Суть | Доказательство |
|---|---|---|---|---|
| **GAP-1** | 1 | 🔴 блокер точности | Татарстан=16 неверно (это 19; 16=Мордовия) | RussianRegions.php:36,39 |
| **GAP-2** | 4 | 🔴 блокер исполнимости | корпус R0–R5/map/хартия untracked → битые ссылки | git ls-files (пусто) |
| **M-1** | 3 | 🔴 HIGH security | админ-зона fail-open, держит только nginx (=F-T2 15.06) | EnsureSaasAdmin.php:35-46 |
| **M-2** | 3 | 🔴 HIGH security | капча самозаписи = Null, реальной нет; план №18 stale | AppServiceProvider.php:54-57 |
| **GAP-3** | 4 | 🟠 | капча-Null ломает тест онбординга №18 как написан | (M-2) |
| F-CSV-wide | 3 | 🟠 | F-CSV шире: +CsvFormatter/XlsxFormatter (4 отчёта×2) | CsvFormatter.php:31-38, XlsxFormatter.php:29 |
| append-only | 3 | 🟡 | lead_charges/supplier_lead_costs без trigger; GRANT off на dev | schema.sql:1143-1181,2589-2613 |
| cover-freeze | 2 | 🟡 | вечерняя заморозка §20 + сохранение ручных пауз — нет шага | — |
| F-REMIND-over | 3 | 🟡 | переоценена: мёртвого тумблера нет | NotificationsTab.vue:31-46 |
| lpimp-status | 3 | 🟡 | lpimp_→401 не 403; /api/billing/charges читается lpimp_ | web.php:193 |
| tenants-RLS | 3 | ✅ СДЕЛАНО 22.06 | tenants без RLS-политики (латентно) — включена RLS + политика `tenants_self_isolation` (gitea `60c640e8`, на прод не выкачено) | schema.sql (нет policy) |
| №23-tx | 1 | 🟡 | шаг «balance_transactions не выросло» неверен | HistoricalImportService.php:255 |
| №12-header | 1 | 🟢 | header схемы «5» vs факт «6» audit-таблиц | schema.sql:81-86 vs :3304-3344 |
| apiv1-rate | 3 | 🟢 | /api/v1/deals без rate-limit (bcrypt/запрос) | DealsController |

---

## Что сделать ДО прод-прогона (приоритет)

1. **🔴 Поправить Татарстан 16→19** в плане №3, своде, PR2 (иначе ложный провал каскада).
2. **🔴 Закоммитить корпус приёмки** (R0–R5/map/хартия/stepbystep) — docs-only, deptrac не мешает — иначе свежая сессия в клоне не стартует. Либо явно: «прогон только в текущей рабочей копии».
3. **🔴 Внести в реестр M-1 (админ fail-open) и M-2 (капча-Null)** как HIGH; решить с владельцем, фиксить ли до продажи (M-1 — выше F-CSV по риску). Сверить прод-`.env` капчи; переформулировать тест №18.
4. **🟠 Расширить F-CSV** на `CsvFormatter`/`XlsxFormatter` (иначе фикс закроет половину поверхности).
5. **🟡 Добавить шаг вечерней заморозки §20** + явный числовой пример share-aware `required` §21.2.
6. **🟡 В план №12** — оговорка «append-only `lead_charges` проверять под `crm_app_user` на проде, dev-superuser не заблокирует»; поправить шаг №23 (сверять `lead_charges`/баланс, не число транзакций).
7. **🟢 Поправить F-REMIND** (мёртвого тумблера нет — только мёртвый тип + коммент); синхронизировать header schema.sql «5→6».

## Вердикт по готовности

- **Money/isolation-прогон может идти** — корректность денег и изоляции в коде подтверждена, дефектов не найдено; планы по ним точны.
- **Но старт — после пп.1–3** (Татарстан, корпус, реестр+капча): без них прогон рискует ложным провалом (Татарстан), не стартует в клоне (корпус) или даст ложный GREEN по капче/append-only.
- **Security-постура (M-1, M-2)** — бизнес-риск, который владелец обязан знать до передачи продажникам, даже если прогон формально пройдёт GO.

---

*Разбор read-only. Прод не тронут, код не правился. Коммит этого отчёта — по слову владельца.*
