# Приёмка liderra.ru — реестр находок (выявлены при чтении кода под планы)

**Дата:** 20.06.2026. **Кодовая фраза стены:** «роутер-наставник».
**Назначение:** зафиксировать всё, что всплыло при сверке свода проверок с реальным кодом во время написания планов-отчётов (PR2…№26). Каждая находка — с доказательством (file:line), важностью и рекомендацией. Это **не дефекты прода по факту прогона**, а расхождения/хвосты, найденные статически; помечены, где нужен живой прогон для подтверждения.

**Связанные:** прогонные находки клиентского осмотра F1–F5 — [docs/superpowers/runbooks/2026-06-17-portal-client-walk-report.md](../runbooks/2026-06-17-portal-client-walk-report.md). Свод проверок — [2026-06-20-acceptance-imitation-checks-table.md](2026-06-20-acceptance-imitation-checks-table.md).

---

## 🔴 Важные (безопасность / деньги) — под проверку и фикс

### F-CSV — Экспорт без защиты от CSV-инъекции (формул)
- **Что:** ячейки выгрузок пишутся «как есть»; экранирования ведущих `= + - @` (нейтрализация формул Excel) в коде **нет** ни в одном из путей экспорта.
- **Где:** сделки — [DealExportController.php:125-132](../../../app/app/Http/Controllers/Api/DealExportController.php#L125-L132) (OpenSpout `Row::fromValues`); отчёты — [DealsExportProvider.php:67-85](../../../app/app/Services/Reports/Providers/DealsExportProvider.php#L67-L85); списания — [TenantChargesController.php:85,133](../../../app/app/Http/Controllers/Api/TenantChargesController.php#L85) (`fputcsv`).
- **Риск:** значение вроде `=HYPERLINK(...)`/`=1+1` в комментарии/контакте при открытии файла в Excel/LibreOffice может **исполниться как формула** (CSV/formula injection).
- **Свод:** утверждал «CSV-инъекция экранирована» — **расходится с кодом**.
- **Рекомендация:** проверить реальным файлом на прогоне; при подтверждении — экранировать ведущий символ (`'` префикс) в писателе экспорта. Затрагивает пункты №14, №11.
- **Пункты:** №14, №11.

---

## 🟡 Расхождения свода с кодом — поправить свод / проверить на прогоне

### F-DELPROJ — Удаление проекта со сделками запрещено (а не «сделки сохраняются»)
- **Что:** проект со сделками **нельзя удалить** — 422 «поставьте на паузу»; удаляется только пустой проект.
- **Где:** [ProjectService.php:186-191](../../../app/app/Services/Project/ProjectService.php#L186-L191).
- **Свод:** «удалить проект со сделками → сделки сохранены» (как будто удаляется). Итог тот же (данные целы), механизм другой.
- **Рекомендация:** поправить формулировку свода №15 на «удаление запрещено при наличии сделок».
- **Пункт:** №15.

### F-DIGEST — ❌ СНЯТА (была моей ошибкой чтения, не дефект)
- **Что (исходно):** прочитал `new_lead {inapp:true, push:true, email:false}` — будто дайджест по умолчанию выключен.
- **Правда:** канон [db/schema.sql:796](../../../db/schema.sql#L796) = `"new_lead": {"inapp":true,"push":true,"email":true}` — дайджест **ВКЛючён по умолчанию** (G2-B 19.06: коммиты `37ad398c` канон схемы + `01dffd6b` миграция, в локальной истории). Клиент получает дайджест без донастройки.
- **Причина ошибки:** **локалка НЕ отстаёт** (деплой `01a9029c` — предок HEAD, байт-в-байт). Я прочитал **устаревший комментарий-докблок** в `NotificationService.php` (ссылался на `schema.sql:699` и `email:false`) вместо канона-схемы. Комментарий не обновили при G2-B.
- **Исправлено:** устаревший комментарий поправлен ([NotificationService.php:25-27](../../../app/app/Services/NotificationService.php#L25-L27)) → теперь `email:true` + корректная ссылка.
- **Урок:** канон — **схема/живой код**, не комментарии. Для прод-теста ключевые числа подтверждать на проде.
- **Рекомендация:** в плане №20 убрать оговорку «под проверку дефолта» — дайджест вкл по умолчанию (канон-схема).
- **Пункт:** №20.

### F-PDF — PDF-формат отчёта: штатность «failed» не подтверждена
- **Что:** формат `pdf` в списке, но генерация может не поддерживаться; ошибка ловится → статус `failed` (без падения).
- **Где:** [GenerateReportJob.php:102](../../../app/app/Jobs/GenerateReportJob.php#L102); форматы — [ReportJob.php:51](../../../app/app/Models/ReportJob.php#L51).
- **Свод:** «PDF→failed штатно» — **не подтверждено** статикой.
- **Рекомендация:** заказать PDF на прогоне; убедиться, что завершается мягким `failed`, а не виснет/роняет.
- **Пункт:** №22.

---

## 🧹 Хвосты уборки (недоделки удалённых фич) — дочистить

### F-REMIND — Остатки снятой фичи «Напоминания» во фронте
- **Что:** база/эндпоинт/меню напоминаний убраны (G3, миграция `drop_reminders_table`), но во фронте остались следы.
- **Где:**
  - `'reminder'` в перечне типов уведомлений — [resources/js/api/auth.ts:12](../../../app/resources/js/api/auth.ts#L12) → риск **мёртвого переключателя** «Напоминания» в Настройках.
  - устаревший комментарий-докблок — [resources/js/components/deals/DealDetailBody.vue:3](../../../app/resources/js/components/deals/DealDetailBody.vue#L3) (клиенту не виден).
- **Не путать:** `billing:frozen-reminder` (письмо о заморозке) и `POLLING_REMINDERS_INTERVAL_MS` (опрос баланса) — это **другое**, не трогать.
- **Рекомендация:** убрать `'reminder'` из `NotificationEventKey` (если рендерит мёртвый переключатель) + почистить комментарий. Правило «не оставляй хвостов».
- **Пункт:** №21.

---

## ℹ️ Прежние прогонные находки (F1–F5, осмотр 17.06) — проверить в этом прогоне
Подробности — [portal-client-walk-report.md](../runbooks/2026-06-17-portal-client-walk-report.md). Кратко, привязка к пунктам:

- **F1** — карточка сделки без поля «Город» (проверить вывод города) → пункт №3/№13.
- **F2** — «Стоимость лида 0 ₽» при реальном списании (цена тянется из `lead_charges`) → пункт №13.
- **F3** — «хватит на дней» Дашборд↔Биллинг расходилось → **исправлено** (единый источник, [DashboardController.php:110](../../../app/app/Http/Controllers/Api/DashboardController.php#L110)); план №11 ловит рецидив.
- **F4** — пустой столбец «Операция» в ленте → пункт №13.
- **F5** — дашборд «средняя 2248 ₽» — мок-значение → проверить реальность числа.

---

## 🧱 Блокеры и инфраструктура (всплыли 20–21.06)

### F-DEPTRAC — Нарушение слоёв блокирует ВСЕ php-коммиты
- **Что:** `App\Http\Middleware\ImpersonationContext` зависит от `App\Mail\ImpersonationEndedMail` — слой Middleware не должен зависеть от слоя Mail (deptrac ruleset).
- **Где:** [app/app/Http/Middleware/ImpersonationContext.php:37](../../../app/app/Http/Middleware/ImpersonationContext.php#L37).
- **Происхождение:** предсуществующее, от деплоя G7-B (impersonation, 19.06). Подтверждено: видно на чистом HEAD (не от текущих правок).
- **Эффект:** pre-commit хук (lefthook → deptrac) валит **любой** коммит, затрагивающий `.php`. Из-за этого не закоммитить даже правку комментария в `NotificationService.php`.
- **Рекомендация:** либо рефакторинг (вынести отправку `ImpersonationEndedMail` из middleware в сервис/джоб — правильно архитектурно), либо обновить `app/deptrac.yaml` ruleset + ADR-005 (если зависимость осознанная). Хук **не обходить** `--no-verify` без явного слова владельца.
- **Пункт:** инфраструктура (затрагивает №25 impersonation).

### F-CORPUS — Корпус приёмки не закоммичен (untracked)
- **Что:** ключевые документы приёмки лежат в рабочей копии как **untracked**, ни разу не в git: R0–R5 ранбуки, хартия, `prod-logic-map.md`, эфир-хэндофф, `imitation-checks-table.md`, `stepbystep-plans.md`, папка `live-demo/`.
- **Эффект:** МАСТЕР-хэндофф (закоммичен `b7e3af4c`) **ссылается на untracked файлы** — свежая сессия в новом клоне их не найдёт; ссылки битые.
- **Рекомендация:** закоммитить корпус приёмки (docs-only — deptrac не мешает), чтобы хэндофф был самодостаточным. Решить с владельцем, что из untracked WIP включать.
- **Пункт:** инфраструктура хэндоффа.

## Сводная таблица

| ID | Важность | Пункт | Статус | Доказательство |
|---|---|---|---|---|
| F-CSV | 🔴 безопасность | №14, №11 | ✅ **закрыто 21.06** (TDD) | `CsvFormulaGuard` нейтрализует `= + - @ \t \r` в DealExport + DealsExportProvider; TenantCharges не уязвим (колонки enum/число/дата) |
| F-DELPROJ | 🟡 свод неточен | №15 | ✅ **закрыто 21.06** | формулировка №15 → «удаление запрещено (422), сделки целы» |
| F-DIGEST | ❌ снята (ошибка чтения коммента) | №20 | канон-схема `email:true`, дайджест вкл | db/schema.sql:796 |
| F-PDF | 🟡 не подтверждено | №22 | ⏸ на прод-прогон | GenerateReportJob.php:102 |
| F-REMIND | 🧹 хвост | №21 | ✅ **закрыто 21.06** | убран мёртвый тип `'reminder'` (auth.ts) + устаревший коммент (DealDetailBody.vue); переключателя в EVENTS не было |
| F-DEPTRAC | 🧱 блокер | инфра/№25 | ✅ **закрыто 21.06** (рефактор) | отправка письма вынесена в `ImpersonationExpiryService`; deptrac 0 нарушений → php-коммиты разблокированы |
| F-CORPUS | 🧱 инфра | хэндофф | ✅ **закрыто 21.06** | корпус приёмки + смежные доки застейджены к коммиту (docs-only); runbooks добавлены в gitleaks allowlist |
| F1–F5 | ℹ️ прежние | №3,№13 | ⏸ на прод-прогон (F3 закрыта) | walk-report 17.06 |

**Итог (обновлено 21.06):** локально закрыто 5 находок — **F-DEPTRAC** (рефактор, deptrac зелёный), **F-CSV** (экранирование формул по TDD), **F-REMIND** (мёртвый тип/коммент), **F-DELPROJ** (формулировка свода), **F-CORPUS** (корпус к коммиту). Хвост-коммент `NotificationService.php` (G2-B) — к коммиту. Отложено на прод-прогон: **F-PDF**, **F1/F2/F4/F5** (закрываются на живом боевом). F-DIGEST — снята ранее. Все код-правки по TDD; deptrac/Pint/Larastan(level 5)/type-check чисты.

> **Локалка в синхроне с боевым** (`01a9029c` — предок HEAD, байт-в-байт). **Урок:** канон — **схема/живой код**, не комментарии-докблоки. F-DIGEST оказалась ложной именно из-за устаревшего комментария (исправлен). Для прод-теста ключевые числа (тарифы, дефолты) подтверждать на проде.

---

## 🆕 Раунд 2 (две независимые ревью-сессии, 21.06) — статусы

Две сессии-ревьюера нашли ещё находки. Где закрыто локально:

- **F-CSV — расширено и ДОзакрыто:** ревью нашёл, что формула уходит не только через DealExport, но и через **центральные писатели отчётов** `CsvFormatter`/`XlsxFormatter` (все 4 типа × csv/xlsx). Защита перенесена в форматтеры (`CsvFormulaGuard::neutralizeCell` + XLSX `TYPE_STRING`); провайдерная нейтрализация откачена (портила JSON). Коммит `862243a2`. **F-CSV теперь закрыт полностью.**
- **GAP-1 Татарстан 16→19** (16=Мордовия), **N-6** воронка-призрак, **№23-tx**, **lpimp-status** (401/403/charges), **append-only** (dev false-GREEN), **капча-M-2** — поправлены в планах/своде (`7be1c93e`).
- **N-1** (CRITICAL teardown), **N-2/N-3/N-8** (R5), **N-5** (R2), **N-7** (R1) — починены в runbook-SQL (`e0fb6e46`).
- **F-REMIND хвост** AppLayout (`c1d61068`).

**Требует решения владельца** (намеренные состояния / смена поведения / объём) — вынесено в [2026-06-21-acceptance-owner-decisions.md](2026-06-21-acceptance-owner-decisions.md): **M-1** (админ fail-open) — ✅ ИСПРАВЛЕНО в коде (подтверждено app/app/Http/Middleware/EnsureSaasAdmin.php:38, проверка 22.06.2026): добавлен fail-closed app-гейт `Route::middleware('saas-admin')` + REMOTE_USER allowlist, **M-2** (капча Null), **N-4** (дайджест-дубль), tenants-RLS, apiv1-rate, `created_at` TZ, **#21** (schema header — блок db/-долга), **Раздел B** (~25 тест-путей). — **ПРОД-СТАТУС 22.06.2026 (проверено по боевому):** ✅ на проде уже развёрнуты M-1 (гейт активен, app.env=production), M-2 (реальная капча Yandex SmartCaptcha, не Null), N-4, apiv1-rate; ❌ всё ещё открыты tenants-RLS (на проде RLS=OFF) и `created_at` TZ (TIMESTAMP без зоны); #21 и Раздел B — за владельцем.

Полный список раунда-2 + статусы — [2026-06-21-acceptance-coverage-expansion-NEW.md](2026-06-21-acceptance-coverage-expansion-NEW.md).
