# Балансовый блок — сессия F/J ЗАКРЫТА (23.06, вечер) + хэндофф следующей сессии

**Дата:** 2026-06-23 (~15:00 МСК). **Кодовая фраза стены:** «роутер-наставник».
**Где работали:** только докалка (локальный dev) + браузер. **На боевой НИЧЕГО не катилось.**

Этот файл продолжает [SESSION-DONE-2026-06-23-and-consolidated-handoff.md](SESSION-DONE-2026-06-23-and-consolidated-handoff.md) — закрывает его §2 (F/J) и §6 п.5. Остальные хвосты оттуда — без изменений (сведены ниже в §2).

---

## 1. ЗАКРЫТО в этой сессии — НЕ ТРОГАТЬ ПОВТОРНО

**На gitea/main коммит `8696b5e2` (8 файлов, +786/−157), НЕ на проде.**
Спека+план: `docs/superpowers/specs/2026-06-23-balance-lock-unify-FJ-design.md` + `docs/superpowers/plans/2026-06-23-balance-lock-unify-FJ.md`.

| # | Что сделано | Файлы |
|---|---|---|
| **D2** | Единый расчёт required = **полный лимит** `sum(daily_limit_target where is_active)` во всех точках. **Откат share-aware R-19** (решение владельца). Стоимость — по ступеням от текущего месячного объёма (Чтение А, конвертер не трогали). | `Tenant::requiredLeadsForTomorrow` (убран импорт DB), `TenantPreflightTest` |
| **B/D3** | Пополнение снимает **оба замка вместе** сразу (клиентский `frozen_by_balance_at` + проектный `preflight_blocked_at`), политика «всё-или-ничего». | `ProjectBlockReleaseService`, `BillingTopupService` (уже звал сервис — не трогали), `ProjectBlockReleaseOnTopupTest` |
| **F** | Вечерний пересчёт **18:00 МСК** (`billing:preflight-sweep`, синхронно) снимает блоки проектов у незаморожённых. | `BalancePreflightSweepJob`, `BalancePreflightSweepJobTest` |
| **J** | Иерархия: заморозка клиента главнее проектного блока; единый расчёт → `passes ⟺ активен ⟺ блоков нет`. | (в B/F) |
| **D6** | Общий путь разблокировки `ProjectBlockReleaseService::releaseForTenant` (зовут и topup, и sweep passes-ветка); sweep оставил только freeze active→frozen. | `ProjectBlockReleaseService`, `BalancePreflightSweepJob` |
| **🔧 fix дедлока** | `balance_freeze_log` INSERT переведён на **главное** соединение. Был межсессионный self-lock: FK→tenants берёт FOR KEY SHARE, а topup держит `lockForUpdate` (FOR UPDATE) на той же строке в главной транзакции; INSERT через отдельное `pgsql_supplier` ждал вечно. **В тестах маскировался `SharesSupplierPdo`** (общий PDO). Подтверждён `pg_blocking_pids` на живом прогоне. `paused_at` (не-FK UPDATE, родителя не лочит) оставлен на `pgsql_supplier`. | `ProjectBlockReleaseService` |

**Проверки:** 138/138 биллинг-тестов GREEN (`tests/Feature/Billing` + `tests/Unit/Billing`), Pint чисто по 3 файлам. **Живьём на докалке:** пополнение через UI сняло баннер «приостановлено» + сменило метку проекта (B); прогон `billing:preflight-sweep` снял блок проекта (F). Скрины `FJ-1..FJ-5.png` (корень репо, тест-данные без ПДн — **не коммичены**). Докалка убрана (tenant 8 удалён, `public/hot` восстановлен, временные `_*.php` удалены, браузер закрыт).

**Деплой-нюанс (вписан в спеку):** переход на «полный лимит» делает заморозку **строже** для клиентов, делящих номер-источник (нужно ×N больше баланса). При накате убедиться, что роль планировщика (`DB_USERNAME` на проде) имеет INSERT на `balance_freeze_log` (есть у app-ролей).

**Квирк окружения:** `php artisan tinker <file>` в цепочке `;` ВИСНЕТ (ждёт stdin) — следующая команда не запускается. Команды (sweep и т.п.) запускать отдельным вызовом.

---

## 2. ОСТАТОК ХВОСТОВ (свести/закрыть в следующих сессиях)

### 🔴 Трогает боевой — нужен GO владельца
- **L** — разовая сверка реальной омеги: tenant 25 / проект 188 стоит `preflight_blocked_at`, но уже заказан у поставщика (b1/b2/b3=494/495/496). Сначала **read-only разведка по SSH** (`ssh liderra-prod`), потом предложить сценарий (снять блок если баланс покрывает / убрать supplier-заказ если не должен), **GO владельца перед записью**.
- **Деплой недеплоенного стека** — прод катился 19.06 (`01a9029c`); на gitea ~**99 коммитов** не на проде: весь балансовый блок (`2deaf207..de106a20` + F/J `8696b5e2`), blocked-create fix `3391cc7a`, source-edit-lock + онлайн-ЮKassa `3b142f93` (флаг `billing_yookassa_enabled`=off), FN-фиксы `e8e5c82b..7fd3cde4`. ДО деплоя — сверка реального HEAD прода по SSH; накат как 19.06 (миграции crm_migrator под postgres-superuser). Перед включением ЮKassa — заполнить `YOOKASSA_WEBHOOK_IPS` + апгрейд до `auth:saas-admin` (висит на Б-1).

### 🔴 Критично к продаже (в коде, поедет с деплоем)
- **FN-RESET** — ~~сброс пароля сломан для всех. Выкатить ДО продажи.~~ **✅ ЗАКРЫТО И НА ПРОДЕ** (проверено живьём 23.06: `createUrlUsing` в `/var/www/liderra/app/app/Providers/AppServiceProvider.php:88`, тест `PasswordResetUrlTest` GREEN, коммит `e8e5c82b`). Эта строка была устаревшей — фикс выкатан ещё 22.06.
- **FN-SESSION** — ~~заход к поставщику через Playwright падает; вероятно поэтому лиды не идут.~~ Браузер/диагностируемость **уже на проде** (21.06 12:36, ошибок PlaywrightBridge с тех пор 0). **«Лиды не идут с 3 июня» — ✅ объяснено: владелец сам выключил проекты (намеренно, подтверждено 23.06) — НЕ дефект, чинить не нужно.** Технический разбор (FN-SESSION починен; молчаливый дроп — мелочь) — [2026-06-23-LEADS-STOPPED-correction.md](2026-06-23-LEADS-STOPPED-correction.md). Ops-профилактика рецидива браузера: `playwright install под www-data` в деплой.

### ✅ Прод-ops — ЗАКРЫТО (сверено живьём 23.06)
- ✅ `failed_jobs` — **очищены** (494к → 7 на проде, флаш сделан 22.06). Не остаток.
- ✅ tenant 24 (`kdv1`) — **удалён** (soft-delete `deleted_at=2026-06-22 12:38`). Не остаток.
- 🟡 (необязательное) playwright install под www-data в деплой-пайплайн — профилактика рецидива браузера; браузеры на проде есть. Чистка m06-инцидентов — самоизлечивается после teardown.

### ✅ Долги инструментов — ЗАКРЫТО / принято (23.06)
- ✅ **deptrac** — ЗАКРЫТО: заведён `app/deptrac.baseline.yaml` (design-механизм самого конфига: baseline фиксирует текущее `ProjectResource→SupplierSnapshotGuard` — read-only расчёт замка для UI; перенос в контроллер усложнил бы коллекции без выигрыша), подключён через `imports`. Прогон: **0 violations, 1 skipped**. Гейт ловит только НОВЫЙ дрейф. (Коммиты больше НЕ нужно гнать с `LEFTHOOK_EXCLUDE=deptrac`.)
- ✅ **larastan** — ПРИНЯТО как env-факт: краснота локально = ide-helper L13/Windows, на CI зелено, не код-дефект (регенерация делает ХУЖЕ — 278). Не остаток, не трогаем. Коммиты с `LEFTHOOK_EXCLUDE=larastan`.
- ✅ **диагностируемость молчаливого дропа** — РЕШЕНО won't-fix: `no_snapshot_skipped` оставлен `Log::info` осознанно. Поднятие до `warning` спамило бы на КАЖДЫЙ лид намеренно-выключенного проекта (владелец выключил проекты сам, поставщик ещё шлёт) = алерт-фатига. Текущее поведение корректно. Пересмотреть только если понадобится видеть НЕОЖИДАННО-выключенные проекты.

### ✅ Приёмка раунд-2 — ВСЁ ЗАКРЫТО (сверено живьём 23.06)
- ✅ **M-1** (fail-closed гейт `/api/admin`, коммит `d784df50`) — **на проде** (saas-admin в роутах + nginx basic auth).
- ✅ **M-2** (капча) — **на проде** (`config:captcha.driver=yandex`, проверено через artisan).
- ✅ **apiv1-rate** (`throttle:api-v1` 120/мин, коммит `255680cf`) — **на проде**.
- ✅ **#21** (schema header v8.51, reminders задокументирован как снятый v8.45; карта разделов — канон-файл).
- ✅ **Раздел B** (закрыт как «не остаток» — реальные гапы покрыты тестами `b9184a6a`+`d7b5f2c1`).
- Авторитетная сводка состояния: `docs/superpowers/specs/2026-06-22-СОСТОЯНИЕ-остатки-и-ключи.md`.

### ⛔ Старый блокер
- **G5 / Б-1** — реквизиты ООО (висяк). От него: настоящая онлайн-оплата (`auth:saas-admin`), Диз-3, DO-2, DO-4.

---

## 3. ПРОМТ ДЛЯ СЛЕДУЮЩЕЙ СЕССИИ (копировать целиком)

```
Кодовая фраза «роутер-наставник». Продолжаем работу по Лидерре (балансовый блок и хвосты).
Контекст последней сессии (F/J + фикс дедлока) и ПОЛНЫЙ остаток хвостов — в:
docs/superpowers/findings/2026-06-23-balance-block/SESSION-DONE-FJ-2026-06-23-and-next-handoff.md
Прочитай его ПЕРВЫМ делом: §1 — что УЖЕ закрыто (НЕ трогать повторно), §2 — остаток хвостов.
Более ранний сводный контекст — SESSION-DONE-2026-06-23-and-consolidated-handoff.md рядом.

УЖЕ ЗАКРЫТО (не делать заново): весь балансовый блок N/G/E/H/M/O (gitea 2deaf207..de106a20)
и F/J + фикс межсессионного дедлока balance_freeze_log (gitea 8696b5e2). Всё на gitea/main,
НЕ на проде. Прод последний раз катился 19.06 — на gitea ~99 недеплоенных коммитов.

Работаем на докалке (как поднять — HANDOFF.md §5; для браузера: Vite лежит, отдавать билд —
`npm run build` + временно убрать public/hot, вернуть в teardown). На боевой НИЧЕГО без явного GO.
Правило: прежде чем чинить — подтверждай код+глаза, что не сделано в другом месте.
Квирк: `php artisan tinker <file>` в цепочке `;` виснет — запускать команды отдельно.

ОСТАЛОСЬ (приоритет уточни у владельца в начале):
  ✅ ЛИДЫ НЕ ИДУТ С 3 ИЮНЯ — НЕ остаток: владелец сам выключил проекты (намеренно, подтверждено
      23.06). НЕ дефект, чинить не нужно. Тех.контекст (FN-SESSION починен, молчаливый дроп —
      мелочь) — findings/2026-06-23-balance-block/2026-06-23-LEADS-STOPPED-correction.md.
  L — омега (tenant 25/проект 188) под preflight_blocked_at (баланс 300₽). Включать ТОЛЬКО если
      владелец захочет (проект выключен намеренно). Тогда — разблокировать (пополнить/снять блок), GO.
  ДЕПЛОЙ недеплоенного стека на прод (балансовый блок + F/J + source-edit-lock):
      (FN-RESET/FN-LOGIN УЖЕ на проде — проверено 23.06, не включать в накат как новое.)
      ДО — сверка реального HEAD прода по SSH; накат как 19.06 (crm_migrator под postgres).
      Деплой-нюанс: «полный лимит» строже морозит клиентов с общим номером; роль планировщика
      должна иметь INSERT на balance_freeze_log.
  ✅ Долги докалки — ЗАКРЫТЫ/приняты 23.06 (НЕ остаток): deptrac = baseline заведён
      (app/deptrac.baseline.yaml, 0 violations/1 skipped); larastan = принят env-факт (CI зелено);
      диагностируемость молчаливого дропа = won't-fix (warning спамил бы на намеренно-выключенные).
  ✅ Прод-ops — ЗАКРЫТЫ 23.06: failed_jobs очищены (494к→7), tenant 24 удалён (soft-delete 22.06).
      Остаётся лишь необязательное: playwright install под www-data в деплой (профилактика) + m06 self-heal.
  (Приёмка раунд-2 — ✅ ВСЯ ЗАКРЫТА, сверено 23.06: M-1/M-2/apiv1-rate — на проде, #21/Раздел B — закрыты. Не остаток.)

Каждый кодовый пункт: TDD (падающий тест → правка → GREEN) + визуал в браузере. Любой результат —
скрин с нашей стороны И со стороны поставщика (если затронут). После блока — коммит сам с GO
владельца; на боевой не пуш без отдельного GO.
```
