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

**Дата:** 21.06.2026. **Кодовая фраза стены:** «роутер-наставник».
**Зачем файл:** часть находок двух ревью-сессий — это **намеренные продуктовые/архитектурные состояния**, **смена прод-поведения** или **крупный объём**. Их нельзя чинить молча — нужно слово владельца. Здесь они собраны с анализом и вариантами. Всё, что можно было безопасно поправить локально, **уже поправлено** (см. реестр + коммиты).

---

## ✅ ЗАКРЫТО В ЭТОЙ СЕССИИ (21.06, локально, не пушено)

- **apiv1-rate — СДЕЛАНО (TDD).** `throttle:api-v1` 120/мин/источник (Bearer-ключ→IP) ПЕРЕД `apikey` на `/api/v1/deals`. Лимитер в `AppServiceProvider`, тест `PublicDealsApiTest` 8/8, Pint/Larastan=0. ✅ ИСПРАВЛЕНО в коде (подтверждено app/app/Providers/AppServiceProvider.php:110-120 + app/routes/web.php:226, проверка 22.06.2026).
- **M-1 — СДЕЛАНО локально (TDD), вариант А (fail-closed app-гейт), allowlist A2 config/env.** `EnsureSaasAdmin` требует непустой `REMOTE_USER` ∈ `config('admin.basic_auth_allowlist')` при `admin.basic_auth_gate` (вкл вне local/testing). Новый `config/admin.php`, тест `EnsureSaasAdminGateTest` 4/4, Pint/Larastan=0. Спека+план в `docs/superpowers/`. **🔴 Деплой — твой шаг: дыра на боевом ОТКРЫТА до выката** (`APP_ENV=production` включит гейт сам, дефолт allowlist=`admin`).
- **M-2 — ✅ СДЕЛАНО, проверено на проде 22.06.2026.** На боевом `CAPTCHA_DRIVER=yandex`, серверный ключ задан, фронт рендерит виджет Yandex SmartCaptcha, бэкенд (`YandexSmartCaptchaVerifier`) делает реальную серверную валидацию у Yandex (`fake_passes` не читается). Капча включена и работает. (Дизайн-нюанс: при недоступности Yandex — fail-open, страхует throttle регистрации.)
- **⚠️ ВАЖНЫЙ ВЫВОД по Разделу B:** при сверке топ-5 оказалось, что док `coverage-expansion-NEW.md` **систематически переоценивает «непокрытое»** — merge, drift-алерты, заморозка с сохранением ручных пауз, идемпотентность, lead==null/partial, retry/cancel/destroy/signed-URL отчётов — **уже покрыты** существующими тестами. Реальные гапы оказались узкими и точечными (ниже). Раздел B как «~25 непокрытых путей» — преувеличение; фактически добавлено 9 целевых тестов на реальные края.
- **Раздел B #2 (заморозка/reminder) — углублён.** Заморозка+ручные паузы+идемпотентность уже покрыты. Добавлены границы reminder/final: dead-zone 48–72h → ничего, >96h → ничего (`BalanceFrozenReminderJobTest`, фальсификация ✓).
- **Раздел B #3 (терминальные пути оркестратора) — углублён.** lead==null (25k-guard)/partial уже покрыты. Добавлены **fast-fail terminal-error** (storm Finding-2) и **all-fail → RuntimeException** (`RouteSupplierLeadJobTest`, фальсификация ✓).
- **Раздел B #4 (жизненный цикл отчётов) — УЖЕ ПОЛНОСТЬЮ ПОКРЫТ.** retry(owner/status/3-попытки/7д/quota), cancel(pending/owner), destroy(terminal/file/NULL/pd-аудит), download(410/404/невалидная+просроченная+tenant-tamper подпись→403) — всё есть. Без добавлений. Незакрыт только quota-TOCTOU (недетерминированная гонка — не характеризуется).
- **Раздел B #5 (G6-edges) — углублён.** 401-варианты/last_used/since-happy уже покрыты. Добавлены: невалидный since→422, невалидный cursor→422, ключ без scope read→403, keyset-пагинация (`PublicDealsApiTest`). Impersonation-границы (lpimp_→403) уже покрыты `ImpersonationDoorTest` + M-1.
- **Раздел B #1 (CsvReconcile/merge) — углублён.** Сверка показала: merge-без-2-го-списания (`CsvWebhookRaceTest`), drift-алерты webhook-loss + business-drift R-05 (`CsvReconcileJobTest`), регион-улучшение при merge (`RouteSupplierLeadJobRegionResolutionTest`) — **уже покрыты** (док B.1 устарел). Единственная неприкрытая денежно-критичная ассерция — **неизменность `received_at` при merge** (regression-guard FK-partition инцидента 26.05) — добавлена в `CsvWebhookRaceTest` (4/4), фальсификацией подтверждено, что тест ловит регресс.

> ⚠️ **Побочно (вне scope M-1):** `AdminSuppliersControllerTest` падает (4 поставщика вместо 3) и на чистом HEAD — предсуществующее состояние тест-БД `liderra_testing`, не регрессия. Требует отдельной чистки сидов.

---

## 🔴 HIGH security — решить ДО передачи продажникам

### M-1 — Админ-зона fail-open (держится только nginx Basic Auth) — ✅ ЗАКРЫТО (см. выше)

✅ ИСПРАВЛЕНО в коде (подтверждено app/app/Http/Middleware/EnsureSaasAdmin.php:54-60 + app/routes/web.php:106, проверка 22.06.2026). ✅ на проде гейт активен (app.env=production, admin.basic_auth_gate=true, проверено 22.06.2026).

> **🚨 Находка 21.06 (доказана curl на боевом):** nginx `^~ /api/admin` ОБХОДИТСЯ через front-controller: `/index.php/api/admin/tenants` → **200** (слив тенантов), `/API/admin/tenants` → **200** (регистр). Прямой `/api/admin/tenants` → 401. Поэтому вариант Б (только nginx) нежизнеспособен — выбран и реализован вариант А (app-гейт по `REMOTE_USER`, закрывает оба вектора независимо от nginx-путей).
- **Что:** `EnsureSaasAdmin::handle()` **никого не аутентифицирует** — только `abort(403)` при активном impersonation/`lpimp_`; иначе `return $next()`. Прежний fail-closed 503 снят осознанно (стопгэп 25.05). Вся защита `/api/admin/*` — на nginx-пароле `.htpasswd-admin`.
- **Где:** [EnsureSaasAdmin.php:35-46](../../../app/app/Http/Middleware/EnsureSaasAdmin.php#L35). **= F-T2** доменного аудита 15.06 (память `project-tails-audit-stage2b`) — находка не новая.
- **Риск:** любой прямой доступ к Laravel мимо nginx (открытый порт контейнера, внутр. сеть, дыра в location-regex) = правка баланса любого тенанта, refund, перезапись вебхук-секрета, 152-ФЗ-erasure, старт impersonation. `init` impersonation — без rate-limit.
- **Почему не правил молча:** восстановление app-слойного гейта = **фича** (нужен app-layer admin-сессионный механизм; стопгэп снят именно потому, что его не было). Рискну запереть админов, если сделать наобум.
- **Варианты:** (А) восстановить fail-closed app-гейт + sessions-механизм (фича, ~1-2 дня); (Б) оставить как есть, но **проверить, что прод-nginx реально покрывает ВСЕ варианты пути** `/api/admin*` (location-regex), и задокументировать как принятый риск; (В) гибрид: быстрый app-гейт «деградирует в 503 если нет валидной admin-сессии».
- **Моя рекомендация:** минимум (Б) сейчас (проверить nginx) + (А) в бэклог как P1 перед массовой продажей.

### M-2 — Капча самозаписи — ✅ СДЕЛАНО, реальный Yandex SmartCaptcha включён на проде (проверено по проду 22.06.2026)
- **СТАТУС (22.06.2026, проверка боевого):** ✅ закрыто. На проде `CAPTCHA_DRIVER=yandex` → биндится `YandexSmartCaptchaVerifier` (реальный, `app/app/Services/Captcha/YandexSmartCaptchaVerifier.php` — HTTP-валидация у Yandex, пустой токен → отказ); серверный ключ в прод-`.env` задан; фронт-сборка `RegisterView-*.js` содержит виджет SmartCaptcha. Прежняя оценка ниже (про `NullCaptchaVerifier`) относилась к коду на 19.06 — **устарела**.
- **Что (на 19.06, устарело):** `CaptchaVerifier` биндился на `NullCaptchaVerifier`; теперь в коде есть `YandexSmartCaptchaVerifier` и `match`-биндинг по `config('services.captcha.driver')`. `NullCaptchaVerifier` пропускает любой непустой токен при `services.captcha.fake_passes` — но он используется только при `driver≠yandex`.
- **Где:** [AppServiceProvider.php:54-57](../../../app/app/Providers/AppServiceProvider.php#L54), `services.php:42`.
- **Риск:** на проде любой непустой `captcha_token` проходит капчу; старт **300₽** на подтверждение = вектор фарма (ограничен лишь подтверждаемым email + throttle).
- **Почему не правил молча:** реальный драйвер = **фича** (нужны ключи Yandex SmartCaptcha + driver-класс + конфиг). Это твоё решение по срокам.
- **Действие:** (1) сверить фактический прод-`.env` капчи (вдруг драйвер дописан после 19.06 — в коде HEAD его нет); (2) решить — ставить реальную капчу до продажи или принять риск с усиленным throttle регистрации. План №18 уже поправлен (формулировка теста).

---

## ✅ N-4 — Дайджест-дубль — ИСПРАВЛЕНО (по твоему «поехали»)

✅ ИСПРАВЛЕНО в коде (подтверждено app/app/Jobs/SendNewLeadsDigestJob.php:59-75, проверка 22.06.2026).

### N-4 — Дайджест новых сделок дублировался при двойном прогоне джоба
- **Что было:** `SendNewLeadsDigestJob` выбирал сделки `received_at > now()-30min` **без маркера отправки** → ручной/повторный прогон (R3b велит дёргать вручную) дублировал письмо.
- **Фикс (без схемы):** идемпотентность **по сделке** через Redis SETNX (`Cache::add('digest_sent:<id>', …, TTL 1 сутки)`) — паттерн как rate-limit `ZeroBalancePausedMail`. Сделка, попавшая в дайджест, повторно не включается. **Схему не трогал** (столбец `digest_sent_at` не понадобился).
- **Где:** [SendNewLeadsDigestJob.php](../../../app/app/Jobs/SendNewLeadsDigestJob.php), TDD-тест «повторный прогон НЕ дублирует» (сюит 5/5 зелёный).
- **Компромисс (осознанный):** маркер в Redis, не в БД — переживает ретраи/двойной прогон; при полном flush Redis 30-мин окно остаётся как fallback. R3b-карточка DIGEST-ON обновлена.

---

## 🟡 Технический долг / hardening (низкий приоритет, твой выбор)

| ID | Что | Где | Почему владельцу |
|---|---|---|---|
| **tenants-RLS** — ✅ СДЕЛАНО 22.06 (gitea `60c640e8`, schema v8.51) | `tenants` без RLS-политики (латентно; сегодня не течёт — нет unscoped-запроса) | schema.sql | RLS-политика = **изменение схемы** (миграция). Включена RLS + политика `tenants_self_isolation` (USING id). На прод не выкачено. |
| ~~**apiv1-rate**~~ ✅ | ~~`/api/v1/deals` без rate-limit~~ — СДЕЛАНО: `throttle:api-v1` 120/мин/источник перед apikey | AppServiceProvider + web.php | закрыто 21.06 (TDD) |
| **created_at TZ** — ✅ СДЕЛАНО 22.06 (gitea `60c640e8`, schema v8.51) | `project_routing_snapshots.created_at TIMESTAMP` (не TIMESTAMPTZ) — нестыковка со стандартом проекта (squawk `prefer-timestamp-tz`); потенц. TZ-дрейф | [schema.sql:2171](../../../db/schema.sql#L2171) | смена типа колонки = **миграция**; сделано той же миграцией `2026_06_22_150000`. ⚠️ ALTER TYPE на партиц. таблице — деплой в окно низкой нагрузки. На прод не выкачено. |

---

## 🛠 #21 — синк комментариев schema.sql (готово, нужен твой коммит)

Правка **готова** (header §7 счётчик audit-таблиц 5→6 + отметка G3 на reminders-комменте, только комментарии, метрики не тронуты), но **Claude-хук её не пускает**: db/ файлы у вас коммитят через терминал (LEFTHOOK=0), поэтому всплыл pre-existing долг — squawk `prefer-timestamp-tz` (created_at выше) + markdownlint MD032 в старой записи CHANGELOG. Бипасить хук сам без твоего слова не стал → откатил из дерева.
**Действие:** скажи «делаю #21» — применю правку и закоммичу через твой терминал-паттерн (или ты сам). Текст правки — в этом отчёте и в реестре.

---

## ✅ Раздел B — расширение охвата тестов — ЗАКРЫТО 21.06.2026

> ✅ **ЗАКРЫТО:** «~25 путей» — переоценка (см. «ВАЖНЫЙ ВЫВОД по Разделу B» выше): большинство уже было покрыто существующими тестами. Реальные точечные гапы закрыты **+9 целевыми TDD-тестами** (коммиты `b9184a6a` B#1 received_at-инвариант + `d7b5f2c1` B#2–#5). Список ниже — исторический контекст. Не остаток.

Отдельный объём работ (не «фикс», а **написание тестов/шагов**). Топ-5 по риску (детали — в [coverage-expansion-NEW.md РАЗДЕЛ B](2026-06-21-acceptance-coverage-expansion-NEW.md)):
1. **CsvReconcileJob** целиком (merge без 2-го списания + drift-алерты) — крупнейшая денежная слепая зона.
2. **Вечерняя заморозка §20 + reminder/final письма** — клиент-видимое, тонкое.
3. **Терминальные пути оркестратора** (`lead==null` — защита от прод-инцидента 25k).
4. **Жизненный цикл отчётов** (retry/cancel/destroy/signed-URL tamper).
5. **Границы impersonation/G6-ключа**.

**Действие:** скажи, браться ли (и в каком объёме) — могу сделать отдельной серией по TDD.

---

## ⏸ Отложено на прод-прогон (не закрывается локально)

- **F-PDF** — мягкий `failed` доказан статически; финально подтвердить на проде.
- **F1/F2/F4/F5** (осмотр 17.06) — город/цена 0₽/пустой столбец/мок-средняя — на живом боевом.

---

*Всё локально-исполнимое уже поправлено по TDD (код) и по сверке со схемой (доки/runbook). Этот файл — только то, что требует твоего решения. Прод не тронут.*
