# Приёмочный тест боевого liderra.ru — рабочая хартия (фиксируем по пунктам)

> **🔄 19.06.2026 — форма показа согласована:** приёмка проводится как **«телевизор-доказательство»** (всё глазами, реальные экраны, два слоя: пошаговая корректность с GO владельца + нагрузка), дизайн — [2026-06-19-acceptance-televizor-proof-design.md](2026-06-19-acceptance-televizor-proof-design.md). Эта хартия (объём проверок по пунктам) и ранбуки R0–R5/R3b остаются источником ЧТО проверяем; новый дизайн описывает КАК показываем и доказываем.

**Дата:** 18.06.2026. Кодовая фраза стены: **«роутер-наставник»**.
**Статус:** ⚠️ ЖИВОЙ РАБОЧИЙ ЖУРНАЛ брейншторма, НЕ финальная спека. Заполняется
пункт за пунктом по мере разбора каждой функции с владельцем (его явная просьба).
Пункты со статусом «ожидает разбора» намеренно не детализированы — их сценарии
определяются в диалоге, выдумывать их вперёд владельца запрещено («не выдумывай»).
Финальная спека теста соберётся из закрытых пунктов на шаге writing-plans.
**Источники истины:** [2026-06-18-prod-logic-map.md](2026-06-18-prod-logic-map.md) (логика прода) + [2026-06-18-test-readiness-handoff.md](2026-06-18-test-readiness-handoff.md) (доступ/находки).

**Цель:** прогнать портал «глазами клиента» на боевом liderra.ru с тест-клиентами, убедиться, что платящему клиенту всё реально работает, ПЕРЕД передачей продажникам.

---

## Общие решения (приняты)

- **Провижининг:** P1 — свежие тест-клиенты через прямой SQL (sudo postgres, owner FLOOR-ESCAPE), маркер `TEST-` в названии, известный пароль, скрипт удаления для teardown. `imitation:seed` на проде запрещён, endpoint создания тенанта нет.
- **Почта (реальные ящики):** `kdv1@bk.ru`, `stels_info@bk.ru` — вешаем на тех тест-клиентов, кому письма реально важны (сброс пароля, новый лид, заморозка). Остальным — «пустой» адрес, токены/факты смотрим в боевой БД read-only.
- **Метод:** глазами в браузере (Playwright) на liderra.ru; поставка лидов — контролируемой инъекцией (живую не гоняем, деньги); деньги/изоляция — сверка в боевой БД read-only.
- **Не трогать:** `lkomega`/tenant 2 (реальный бизнес), живую поставку поставщика, секреты в чат.

### Защита и порядок при дефектах (safety-stop)

- **Любая мутация прода — только через owner FLOOR-ESCAPE.** Read-only SELECT — в разговорном режиме.
- **Защита реального бизнеса:** если любое действие может задеть `lkomega`/tenant 2, реальные домены/источники поставщика или живую поставку — **немедленный стоп**, не продолжать, доложить владельцу. Инъекции — только на тест-домены с маркером `TEST-`, не пересекающиеся с реальными источниками.
- **При критдефекте** (деньги расходятся, изоляция течёт, чужие данные видны): зафиксировать «ожидали / получили / факт из БД», **остановить прогон по этому пункту**, доложить владельцу. НЕ «чинить на ходу» на боевом.
- **UX-косяки** (отображение, тексты) — записывать отдельным списком, прогон не блокируют.

### Teardown (детально — выполняется в конце сессии)

Порядок строгий: сначала чистим у поставщика, потом БД.
1. **rt-проекты у поставщика:** удаление тест-проекта в портале → авто-джоб `DeleteSupplierProjectJob` чистит rt-проект на crm.bp-gr.ru; либо админ-инструмент bulk-delete списка `supplier_projects`. Проверить глазами, что тест-rt-проекты исчезли у поставщика.
2. **Слепки инъекций:** удалить строки слепка за тест-даты (`snapshot:rebuild` за дату или прямой DELETE по тест-проектам, owner-escape).
3. **Тест-тенанты и их данные:** SQL-скрипт DELETE по маркеру `TEST-` с каскадом по зависимым таблицам тенанта (deals/projects/project_supplier_links/snapshots/lead_charges/balance_transactions/supplier_lead_costs/notifications/reminders/report_jobs и пр.). Скрипт удаления готовится вместе со скриптом провижининга (парный, обратимый).
4. **Сессии/кэш:** при необходимости почистить тест-ключи в Redis (throttle входа, dedup вебхука).
5. **Проверка:** после teardown — read-only SELECT, что тест-тенантов и тест-проектов в БД не осталось, у поставщика тест-rt-проектов нет.

---

## Полный перечень функций клиента (что тестируем — ✅ клиентское)

1. Вход + 2FA + сброс пароля
2. Проекты: создать / править / удалить / массовые / проверка баланса
3. Деньги: лента денег + выгрузка + калькулятор «хватит на…»
4. Сделки: лента / карточка / правка / ручное создание / статусы / массовые / экспорт
5. Колокольчик + email о новом лиде
6. Напоминания (создание работает, рассылка ⛔ G3)
7. Отчёты (4 типа, выгрузки)
8. Импорт сделок из CSV
9. Получает письма о заморозке/нуле баланса (фон шлёт, клиент видит)
10. Видит только своё (изоляция — проверяется его глазами)

Заведомо сломанное (НЕ тестируем, знаем): G1 самозапись · G2 исходящие вебхуки · G3 рассылка напоминаний · G4 push · G5 пополнение/счета · G6 API-ключи · G7 impersonation · PDF-отчёт.

---

## Журнал решений по пунктам

### Пункт 1 — Вход + 2FA + сброс пароля ✅ ЗАКРЫТ

**Клиент:** свежий через SQL (P1), маркер `TEST-`, известный пароль.

- **1.1 Вход (логин/пароль)**
  - Что: правильный логин+пароль → в свой кабинет; неправильный → не пускает; **5 неверных попыток / 15 мин → блокировка**.
  - Как: браузером на liderra.ru.
  - Блокировку — **последним шагом** пункта (после неё аккаунт залочен 15 мин или сброс счётчика в Redis). Обратимо.
  - Критерий успеха: верный пароль пускает; неверный — нет; на 6-й попытке — блокировка.
- **1.2 Двухфакторка (2FA TOTP)** — включена в прогон.
  - Что: подключить (QR/секрет) → подтвердить код → вход требует код → код восстановления работает → отключить под паролем.
  - Как: браузером; 6-значный код считаем сами из секрета (TOTP-математика). Полностью обратимо (в конце отключаем).
  - Критерий успеха: после включения вход без кода невозможен; верный код пускает; recovery-код срабатывает; отключение возвращает обычный вход.
- **1.3 Сброс пароля** — делаем ПОЛНОСТЬЮ, с реальным письмом.
  - Что: «забыл» → реальное письмо со ссылкой → новый пароль → вход новым; + анти-перебор (нейтральный ответ на несуществующий email); + TTL ссылки 60 мин.
  - Как: браузером запускаем; письмо ловим на `kdv1@bk.ru`/`stels_info@bk.ru`. Анти-перебор — без почты.
  - Критерий успеха: письмо реально доходит; по ссылке пароль меняется; вход новым работает; на несуществующий email — тот же нейтральный ответ.

**Поправка факта:** «письма о входе» в коде НЕТ. Клиенту уходят только: сброс пароля, новый лид (по подписке), заморозка/ноль баланса.

**Почта пункта 1:** `kdv1@bk.ru`, `stels_info@bk.ru`.

---

### Пункт 2 — Проекты ✅ ЗАКРЫТ (объём согласован)

База фактов — §18 [prod-logic-map.md](2026-06-18-prod-logic-map.md) (глубокое чтение движка 18.06).

**Цель и граница:** доказать, что фича «проекты» корректно работает на ткани многих клиентов (создание/правка/удаление → правильный заказ у поставщика → правильная конфигурация раздачи → деньги не теряются), и понять производительность/нужное железо. Безопасность/юр — вне.

**Базовые решения:**
- Реальный заказ доводим до поставщика и смотрим глазами, НО тест-проекты вычищаем **до 21:00 МСК** (иначе поставщик начнёт слать реальные платные лиды). Один реальный сквозной прогон — в самом конце.
- Раздачу проверяем **инвариантами** (жребий в проде CSPRNG — поимённо не предсказать).
- Заказ сайта = **3 rt-проекта** B1/B2/B3 (заказ÷3, largest-remainder).
- 89 регионов — **сверкой таблицы** `SupplierRegions` (биекция на 79) + кейс Московской области (не поддержана → гео-фильтр теряется).
- Нагрузка идёт **последовательно** (один воркер `liderra-queue.service`) — отдельный слой замеров.
- Провижининг **P1** (SQL, маркер `TEST-`), парный скрипт удаления. Живые почты: c1=`kdv1@bk.ru`, c19=`stels_info@bk.ru`.

**Популяция — 20 тест-клиентов:**

| Клиенты | Источник / тип | Регионы | Лимиты | Назначение |
|---|---|---|---|---|
| c1–c7 (7) | сайт-1 | все Москва | по 10 | шеринг + «второй круг» + заказ (⌈70/3⌉=24 → B1/B2/B3 по 8); c1 почта kdv1@bk.ru |
| c8–c12 (5) | сайт-2 | Москва / СПб / Казань / вся РФ / непокрытый | 20 | каскад фаз 1→2→3 |
| c13 (1) | сайт-3 | Московская **область** | 10 | граница регионов (гео-фильтр теряется) |
| c14–c17 (4) | звонок / смс+слово / смс / DIRECT | Москва | 10 | типы сигнала, площадки, DIRECT без pivot |
| c18–c19 (2) | свои | разн. | c18=2 (перелив), c19 малый баланс | лимит / заморозка / дни; c19 почта stels_info@bk.ru |
| c20 (1) | отдельный | — | 10 | изоляция |

**Слой 1 — механика одного проекта (CRUD), на представителях:** создать · preflight баланса (хватает/нет) · править (лимит/регионы/дни/пауза/источник) · иммутабельность типа сигнала · уникальность имени/источника · удалить + grace-защита · массовые действия (≤500).

**Слой 2 — ткань 20 клиентов (главное):** заказ/деление по площадкам/регионы/каскад строим конфигурацией, проверяем заказ у поставщика **глазами** + раздачу **инъекцией**:
- шеринг: каждый лид ≤3 разных клиента, лимит не превышен, за ~15 лидов обслужены все 7;
- каскад: точный регион → «вся РФ» добор (фаза 2) → подмена (фаза 3);
- площадки B1/B2/B3 по типам сигнала, DIRECT без pivot;
- перелив при исчерпании лимита; маска дней без сегодня → не в слепке.

**Слой 3 — корректность при совмещении:** правки проектов и поток лидов разом → лиды доходят, списания верные, заказ обновляется, ничего не теряется. Без замеров скорости.

**Слой 4 — производительность и ёмкость (инженерный замер, последним):**
- D1 — завал N правок, 1 воркер → проектов/мин.
- D2 — тот же завал при 2/4 воркерах → ускорение **+ проверка гонки группы** (не побилось ли состояние у поставщика).
- D3 — инъекция M лидов → лидов/мин + загрузка сервера.
- D4 — правки + лиды разом → пик нагрузки → расчёт железа.
- Нужно: снятие метрик сервера (CPU/память/база), временное изменение числа воркеров (**FLOOR-ESCAPE**, спокойное окно, вернуть как было), тест-баланс под инъекцию.

**Дополнительные проверки (берём ⭐; бонусы — опционально):**
- Устойчивость: ⭐ авто-восстановление сессии (протухла → сама перелогинилась) · ⭐ восстановление после ручного удаления rt-проекта у поставщика · падение по ярусам 1→2→3+алерт (опц., сложнее).
- Деньги: ⭐ идемпотентность (один лид дважды → одно списание) · ⭐ CsvReconcile (потерянный лид подобран из CSV-отчёта + алерты дрейфа) · ⭐ переход тарифной ступени (цена меняется на нужном по счёту лиде) · ⭐ граница баланса (0 < баланс < цены → списание падает → автопауза) · ⭐ один клиент с двумя проектами на источнике → лид списывается **один** раз (DISTINCT по тенанту).
- Инварианты: ⭐ «слепок после 18:02 → сегодня лидов нет» · ⭐ пауза на лету (R-09: лид на только что паузнутый проект не уходит) · ⭐ «вся РФ» в группе перебивает гео · grace-защита удаления (опц.) · группа на паузе (лимит сохраняется, опц.) · регион-подмена — какой город в сделке (опц.) · объединение дней группы (опц.) · авто-привязка корня домена (опц.).
- Резолвер региона: DaData → Россвязь → тег на разных телефонах + бюджет-гард (опц.).
- Доказуемость: уведомление о новом лиде реально доходит (живой ящик) · аудит-цепочка цела после прогона (`audit:verify-chains`).

NB: часть денежных проверок (тарифная ступень, граница баланса) формально относится к пункту 3, но прогоняется здесь на инъекции пункта 2.

**Teardown пункта 2:** rt-проекты у поставщика (авто-чистка при удалении / админ bulk-delete) → строки слепка за тест-даты → DELETE тест-тенантов по маркеру `TEST-` (каскад) → тест-ключи Redis → проверка, что чисто.
### Пункт 3 — Деньги ✅ ЗАКРЫТ (объём согласован)

База — §6/§8 [prod-logic-map.md](2026-06-18-prod-logic-map.md). **Метод:** сверка «экран клиента ↔ боевая БД» (read-only) на клиентах с историей списаний из инъекции пункта 2. Деньги — тест-баланс, не реальные.

**Цель/граница:** корректность того, что клиент ВИДИТ в кабинете + целостность денег. Механика списания (ступень, граница баланса, идемпотентность, двойной проект) — в инъекции пункта 2.

**Клиентское отображение:**
- 3.1 **Лента денег** (`TenantChargesController`): каждый лид → одна строка (когда/сделка/ступень/источник/цена/остаток после); `остаток после` падает монотонно и сходится копейка-в-копейку; число строк = числу списаний в БД (`lead_charges`/`balance_transactions`).
- 3.2 **Выгрузка CSV** ленты — строки/суммы = экрану и БД; формат `;`+BOM.
- 3.3 **Калькулятор «на сколько лидов хватит»** (`BalanceToLeadsConverter`) = ручной расчёт по ступеням T1–T7.
- 3.4 **Калькулятор «на сколько дней хватит»** (`RunwayCalculator`) = доступные лиды ÷ средняя скорость 30д.
- 3.5 **Дашборд ↔ кошелёк согласованы** (общий `BalancePreflightService`).

**Перепроверка прошлых находок (17.06):** F2 карточка «Стоимость лида 0₽» → реальная цена · F3 дашборд «хватит на дни» = биллингу · F5 «средняя» — реальная, не мок.

**A. Целостность списания (берём ⭐⭐):**
- ⭐⭐ тройная запись согласована (`lead_charges` + `balance_transactions` + `supplier_lead_costs`, суммы бьются, нет полусписаний);
- ⭐⭐ копейки сходятся (bcmath): Σ списаний = начальный − текущий баланс, без потерь дробей;
- ⭐ атомарность/откат: нехватка баланса → полный откат, без хвостов (ни charge, ни движения, ни сделки).

**B. Тарифные ступени (берём ⭐):**
- ⭐ цена по месячному счёту (delivered_in_month+1);
- границы нескольких ступеней (частично, сколько потянем инъекцией);
- сброс 1-го числа → счётчик в 0, цена → T1 (логику по БД).

**C. Баланс-операции (берём ⭐):**
- ⭐ установка баланса админом (set absolute, append-only + аудит, старое не затёрто);
- ⭐ возврат за лид (баланс растёт, запись в ленте);
- пополнение — заглушка (G5), подтверждаем, не чиним.

**D. Провенанс/отображение (берём ⭐):**
- ⭐ источник в ленте `rub` vs `csv_recovery` (восстановленные помечены верно);
- ⭐ карточка сделки `cost_kopecks` = реально списанному (F2).

**E. Что НЕ трогает деньги (берём ⭐):**
- ⭐ ручная сделка → баланс не меняется, в ленте денег нет записи;
- импорт CSV → баланс не списывается (пересечётся с пунктом 8).

**F. Калькуляторы — края (бонус):** новый клиент без истории не падает; замороженный → 0/заморожен.

**G. Аудит денег (берём ⭐):**
- ⭐ денежные таблицы в hash-chain → после прогона `audit:verify-chains` зелёный;
- запись ledger нельзя править/удалять (`audit_block_mutation`) — пробуем, убеждаемся в блоке.

**H. Экономика (отчёт, не тест):** сводка `supplier_lead_costs` (себестоимость) vs `lead_charges` (выручка) → маржа на лид по прогону — для владельца.
### Пункт 4 — Сделки ✅ ЗАКРЫТ (объём согласован)

База — §8 + §19 [prod-logic-map.md](2026-06-18-prod-logic-map.md). Главный экран продажника + механизм раздачи лида в сделки. Гоняется на реальных сделках из инъекции пунктов 2–3 (общий стенд, не дублируем провижининг). Метод: экран клиента ↔ боевая БД (read-only).

#### Слой A–H — рабочий экран сделок
- **A. Лента:** все поставленные лиды → сделки (число = БД); фильтры статус/проект/менеджер/поиск/даты; ⭐ пагинация стабильна (keyset+offset, не теряет/не дублирует); партиции по месяцам открываются; `is_test=false` видны.
- **B. Карточка:** телефон/источник/**город (реальный регион)**/статус/**цена** (`cost_kopecks`≠0, F2); ⭐ события полны (до 50); ⭐ город при подмене реальный; источники rub/csv_recovery — цена корректна.
- **C. Правка + воронка:** правка коммент/менеджер/статус → событие; ⭐ воронка Новая→Просмотрено→В работе→Сделка/Не реализовано (`StatusRuToSlugMapper`); ⭐ «Сделка»(won) НЕ списывает повторно.
- **D. Ручное создание:** ⭐ баланс не трогается, в ленте денег пусто, события есть, визуально отличима.
- **E. Массовые:** ⭐ статус/мягкое удаление/восстановление (≤1000, аудит); удалённые скрыты, restore возвращает; граница 1000.
- **F. Экспорт CSV/XLSX:** ⭐ колонки Телефон/Источник/Город/Статус/Комментарий/Поставлен; `;`+BOM; = ленте и БД; с учётом фильтров; стриминг большого объёма.
- **G. Изоляция:** ⭐⭐ клиент A не видит сделки B (ни в ленте, ни по прямой ссылке/ID → 404/403); RLS + явный фильтр.
- **H. Аудит:** ⭐ события в hash-chain (`ActivityLog`) → `audit:verify-chains` зелёный; правка/удаление лога заблокированы.

#### Блок «Распределение лида в сделки» (механизм раздачи, I–VI)
- **I. Идемпотентность:** ⭐⭐ один лид (vid) = один раз (повтор → skip, без лишних сделок/списаний); ⭐ одна поставка клиенту = один раз (`supplier_lead_deliveries`); ⭐ merge с CSV-recovery (без второго списания); терминальные случаи (удалён/mismatch → fast-fail, без шторма).
- **II. Кому:** ⭐ CAP≤3; ⭐ каскад шаг1 точный → шаг2 «вся РФ» → шаг3 подмена; ⭐ лимит из слепка под локом (R-04/R-06); ⭐ пауза под локом (R-09); нет слепка → не доставляется.
- **III. Что создаётся:** ⭐ сделка (статус new, phones[], дата из payload, город реальный, region_substituted на шаге 3); ⭐⭐ списание в той же транзакции, нехватка → полный откат + автопауза + письмо (1/час); ⭐ счётчики delivered_today/month/snapshot +1; следы ActivityLog+PdAudit+уведомление.
- **IV. Парсинг project:** B1/B2/B3→площадка, без префикса→DIRECT; 7XXXXXXXXXX→звонок, домен→сайт, домен в тексте→сайт, иначе→смс. ⭐ проверка на каждом типе + грязный ввод.
- **V. Резолв региона + аудит:** DaData→Россвязь→тег; одна строка аудита на лид (телефон маскирован), fail-safe.
- **VI. Сбои:** все упали → retry 3× → failed_webhook_jobs; частичный успех не падает целиком.

#### Edge-проверки сделок (берём ⭐)
- ⭐ **Партиции/границы месяца:** старые сделки открываются; фильтр/экспорт через границу месяца не теряют строк; лид с прошлой/будущей датой → нужная партиция / понятная ошибка.
- ⭐ **Дата сделки = время поставки** (payload.time), не создания.
- ⭐⭐ **Мягкое удаление НЕ откатывает деньги** (списание/события остаются, баланс не возвращается); restore целостен; двойное удаление/restore.
- ⭐ **Сделки переживают удаление проекта** (история сохраняется).
- ⭐ **Запрещённые/обратные переходы статусов** (машина состояний, в т.ч. массовой сменой).
- ⭐ **Сделка из CSV-merge** — одна сделка, один источник, одна цена.
- ⭐ **Экспорт-безопасность:** CSV-инъекция (`=`/`+`/`-`/`@`) экранирована; XSS чужого ввода в карточке/экспорте; CSV↔XLSX паритет (кириллица/спецсимволы/даты).
- Поиск краевые (частичный/кириллица/пусто/по любому из phones[]); счётчики/бейджи сходятся после массовых/удалений; `region_substituted` — что видит клиент (решить скрыто/показано); `city=null` не падает; события >50; уведомление→deep-link на карточку; конкурентная правка (последний/лок).

**Граница:** часть проверок изоляции (G) перекликается с пунктом 10 — здесь на денежном экране. Деньги — тест-баланс.
### Пункт 5 — Колокольчик + email о новом лиде ✅ ЗАКРЫТ

База — `NotificationService` (прочитан 18.06). Матрица 8 событий × 3 канала в `users.notification_preferences`.

> **🔄 ОБНОВЛЕНО 19.06 (сверено с боевым `01a9029c`) — каноничные факты пункта; исполняемая форма — [R3b](../runbooks/2026-06-18-acceptance-r3b-onboarding-digest.md):**
> - **Дефолт new_lead теперь `{inapp:true, push:true, email:true}`** — письмо **ВКЛЮЧЕНО** по умолчанию (G2-B флипнул дефолт; проверено `column_default` users.notification_preferences). Клиент получает дайджест без донастройки.
> - **Письмо нового лида = ДАЙДЖЕСТ раз в 30 мин** (`SendNewLeadsDigestJob`, `->everyThirtyMinutes()`), не пер-лид. `notifyNewLead` шлёт только колокольчик на каждую сделку.
> - **Узкое место H1 УШЛО:** синхронного `Mail::send` в транзакции доставки больше нет (email вынесен в 30-мин джоб). Слой D пересмотреть — см. R3b «Заметки для слоя D».
> - push мёртв (G4); zero_balance-письмо осталось; topup/invoice не подключены (G5); напоминаний нет (G3 — удалены, см. пункт 6).

**Находки (исходные 18.06 — частично устарели, см. блок выше):**
- ⭐ **Email о новом лиде по умолчанию ВЫКЛЮЧЕН** (email=false) → клиент получает только колокольчик, пока сам не включит подписку. Типовая будущая жалоба «где письма».
- **push мёртв (G4)** — не приходит, клиенту не обещать.
- Письмо реализовано только для new_lead (по подписке) и zero_balance (заморозка). topup/invoice — не подключены (G5); reminder-рассылки нет (G3).
- ⚠️ **Риск производительности (слой D):** письмо нового лида шлётся **синхронно ВНУТРИ транзакции доставки** (`Mail::send`, не queue) → медленный SMTP держит лок тенанта и тормозит единственный воркер.

**Колокольчик (in-app):**
- ⭐ новый лид → уведомление у **всех активных** пользователей с `new_lead.inapp=true` (заголовок «Новый лид — {проект}», тело = контакт/телефон, ссылка на сделку);
- ⭐ счётчик непрочитанных растёт; отметить прочитанным / все / удалить;
- ⭐ изоляция: пользователь видит только свои уведомления (RLS);
- получатели — только активные не удалённые; ⭐ сбой канала не роняет сделку; тенант с 2 юзерами → колокольчик у обоих.

**Email о новом лиде:**
- ⭐ по умолчанию письма НЕТ (показать);
- ⭐ включаем `new_lead.email=true` → письмо реально приходит на `kdv1@bk.ru` (проект, телефон/контакт, ссылка на liderra.ru, бренд «Лидерра», не спам);
- управление подпиской из настроек меняет доставку.

**Deep-link:** клик по уведомлению → карточка этой сделки. **Push (G4):** подтверждаем, что не приходит.

### Пункт 6 — Напоминания УБРАНЫ целиком ✅ ЗАКРЫТ (G3 исполнен)

> **🔄 ОБНОВЛЕНО 19.06 (сверено с боевым `01a9029c`) — каноничный факт; исполняемая форма — [R3b](../runbooks/2026-06-18-acceptance-r3b-onboarding-digest.md) карточка REM-GONE:**
> Решение владельца «убрать напоминания из портала совсем» **исполнено полностью**. На боевом проверено: нет `ReminderController.php`, нет команды `RemindersDispatchDue.php`, нет `reminders:dispatch-due` в `console.php`, **таблица `reminders` удалена**, раздела в UI нет.
> **Пункт 6 = убедиться, что напоминаний нет** (раздел/URL/эндпоинт/таблица отсутствуют) — НЕ «проверять рассылку». Прежние формулировки (и «подтвердить, что не шлются», и «проверить, что рассылаются») — **сняты как устаревшие**.
### Пункт 7 — Отчёты ✅ ЗАКРЫТ

База — §8 (`ReportJobController` + `GenerateReportJob`). Гоняем на данных из инъекции пунктов 2–4. Метод: отчёт ↔ БД (сверка).
- ⭐ **4 типа** строятся, содержимое верное (сверка с БД): сделки (deals_export), биллинг (billing_summary), источники (sources_summary), менеджеры (managers_summary).
- ⭐ **3 формата** CSV / XLSX / JSON — каждый формируется, числа совпадают между форматами и с БД.
- ⛔ **PDF — заглушка → failed:** падает штатно (не виснет), клиенту не предлагаем.
- ⭐ **Очередь, квота 3:** 4-й одновременный отклоняется/ждёт; повтор / отмена / удаление.
- ⭐ **Скачивание signed-URL 24ч:** ссылка работает; по истечении — нет; чужой не скачает (подпись/изоляция).
- **Изоляция:** отчёт содержит только данные своего тенанта.
- **Большой объём** (вся инъекция) не падает; edge: CSV-инъекция/кириллица/спецсимволы; пустой отчёт не падает.

**Граница:** деньги/сделки уже проверены (п.3–4), здесь — корректность агрегации + изоляция + выгрузка.
### Пункт 8 — Импорт сделок из CSV ✅ ЗАКРЫТ

База — §8/§14 (`ImportController` + `ImportLeadsJob` + `HistoricalImportService` + `CsvLeadsParser` — 9 колонок, дата `Y/m/d H:i:s`). Гоняем тест-файлами.
- ⭐ **Импорт работает:** CSV → сделки в ленте, поля верные.
- ⭐⭐ **Баланс НЕ списывается** — импорт истории не трогает деньги (ключевая защита).
- ⭐ **Идемпотентность:** повторная загрузка того же файла → без дублей (upsert через `webhook_dedup_keys`).
- ⭐ **Мастер незнакомых статусов:** статус из файла вне воронки → сопоставление на new/viewed/in_progress/won/lost.
- **Валидация:** битый формат / лишние-недостающие колонки / плохая дата/телефон → понятная ошибка, не падение; частично-валидный файл — что со строками-ошибками.
- **Изоляция:** импортированные сделки только в своём тенанте.
- Edge: кириллица/кодировка/разделитель/BOM; большой/пустой файл; CSV-инъекция в значениях.

**Граница:** разовая загрузка истории; главное — деньги не трогаются и дублей нет.
### Пункт 9 — Письма заморозки / ноль баланса ✅ ЗАКРЫТ

База — §20 [prod-logic-map.md](2026-06-18-prod-logic-map.md). Пять писем баланса. Гоняем на **c19** (малый баланс), письма на **stels_info@bk.ru**. Триггерим командами вручную + бэкдейт `frozen_by_balance_at` для окон reminder/final (owner-escape).

**Два триггера:**
- ⭐ **Мгновенный (в доставке):** баланс < цены → откат + автопауза + `ZeroBalancePausedMail` (1/час).
- ⭐ **Вечерний sweep @18:00** (`BillingPreflightSweepCommand`): `requiredLeadsForTomorrow` vs ёмкость — **смотрит на завтра**, может заморозить и с положительным балансом. Не хватает → freeze + `BalanceFrozenMail`; хватает → unfreeze + `BalanceUnfrozenMail`.

**Тонкости (⭐):**
- ⭐⭐ **Разморозка восстанавливает только авто-паузы.** Freeze паузит непаузнутые (`paused_at=freezeAt`); unfreeze снимает только `paused_at >= frozenAt`. **Ручная пауза клиента ДО заморозки сохраняется.** Тест: ручная пауза A → freeze паузит B,C → unfreeze → B,C ожили, A на паузе.
- ⭐ **Идемпотентность (анти-спам):** стабильное состояние писем не шлёт; sweep дважды → одно письмо.
- ⭐ **Guard при заморозке:** пока заморожен, удалить/сменить источник нельзя (`SupplierSnapshotGuard`).
- ⭐ **Повторные письма с актуальным дефицитом:** reminder 24–48ч / final 72–96ч пере-считывают дефицит; throttle один marker/тип за 5 дней.
- ⭐ Письма реально доходят на stels_info@bk.ru (бренд, ссылка, не спам).
- ⚠️ Капасити-связь (слой D): freeze/unfreeze диспатчит синк по каждому проекту.

### Пункт 10 — Изоляция (глазами клиента) ✅ ЗАКРЫТ

С учётом §19: supplier-flow/sweep — через `crm_supplier_worker` (BYPASSRLS) + `SET LOCAL` пер-тенант; кабинет — всегда под RLS. Прогон под двумя тенантами (c1 и c8).
- ⭐⭐ **Каждый раздел — только своё:** сделки, проекты, слепки, лента денег, списания, отчёты, уведомления, напоминания, баланс, activity-log.
- ⭐⭐ **Шеринг не ломает изоляцию.** Лид раздан c1,c2,c3 → каждый видит **только свою** копию сделки со своей ценой, не видит, что лид ушёл другим. Cross-tenant раздача в движке ≠ утечка в кабинет.
- ⭐ **Прямой доступ по чужому ID/URL** (сделка/проект/отчёт/уведомление/напоминание) → 404/403.
- ⭐ **Двойная защита** RLS + явный `where(tenant_id)`. Негативный тест: чтение чужой строки как `crm_app_user` без/с чужим контекстом → пусто.
- ⭐ **Глобальные таблицы не текут в кабинет:** `supplier_leads`/`supplier_lead_deliveries`/`failed_webhook_jobs` (BYPASSRLS) клиенту не видны.
- RLS-политики на всех tenant-таблицах (сверка по схеме); джобы корректно ставят `SET LOCAL` перед записью. ПДн (pd) изолированы; impersonation (G7) сломан → не вектор.

---

## ✅ ХАРТИЯ СОБРАНА — все 10 клиентских пунктов закрыты (объём согласован). Следующий шаг — writing-plans (расписать тест пошагово).

### Открытые решения владельца (перед/во время плана)
- **G1 онбординг клиента** — блокер продажи (нет самозаписи): процедура ручного заведения тенанта или wizard. Решить.
- **G3 рассылка напоминаний** — чинить (1 строка) или оставить дырой.
- Слой D (производительность/железо) — детальный план ниже; этап «замер» делаем, D2 (multi-worker) отложен.

---

## Слой D — производительность и ёмкость (детальный план)

**Архитектурная база (из §18/§19/§20):** один воркер (systemd `liderra-queue.service`, `queue:work redis`) обслуживает ВСЁ последовательно — раздачу лидов, синки проектов поставщику, письма, сверку CSV. Письмо нового лида шлётся **синхронно внутри транзакции доставки** (`Mail::send`).

**Цель (бизнес):** 10 000 лидов/сутки (≈7/мин среднее). Проектный потолок кода — 30k; меряем против 10k, фиксируем запас к 30k.

**Главный вопрос:** держит ли ОДИН воркер 10k/сутки с запасом; какой безопасный потолок (лидов/сутки) на текущем железе; при каком темпе очередь начинает расти.

**Сейчас — только замер на одном воркере. D2 (несколько воркеров) отложен.**

**Узкие места (гипотезы):** H1 синхронное письмо в транзакции доставки (главная) · H2 один воркер как потолок · H3 латентность HTTP к поставщику · H5 письма-в-очереди конкурируют с раздачей · (H4 гонка группы — только при multi-worker, в D2).

**Метрики — 3 стороны (read-only):**
- Наша: глубина очереди во времени (Redis LLEN), время задания по типам (метки логов), CPU/память (`/proc/loadavg`, `/proc/meminfo`), БД (`pg_stat_activity`, `pg_locks`, длительность транзакций).
- Канал: ярус (AJAX/форма/failover), повторы, обновления сессии, латентность HTTP.
- Поставщик: создались ли rt-проекты, коды ответов, троттл/блок.

**Эксперименты (этап «замер»):**
- **D1 — База:** завал N заданий (отдельно правки проектов; отдельно инъекция N лидов) → пропускная (лидов/мин, синков/мин), время/задание, глубина очереди. H1: поток клиенту с email-подпиской vs без.
- **D3 — Поток + нагрузка:** инъекция M лидов на целевом темпе (~7/мин) и на пике → CPU/память/БД; растёт ли очередь.
- **D4 — Пик:** правки проектов + поток лидов разом → худшая нагрузка.

**На выходе:** «один воркер держит X/сутки безопасно; 10k = Y% загрузки; запас до Z; узкое место — …; когда добавлять воркеры/железо».

**Отложено (отдельный заход, owner-escape):** D2 — 2/4 воркера + проверка гонки группы (скорость vs корректность); правка systemd, окно, откат.

**Условия:** метрики read-only; окно — прод сейчас спит (0 активных проектов) → идеально, не конкурируем с lkomega. Инъекция тратит тест-баланс (не реальные деньги поставщику); синки проектов реально идут поставщику (нагрузка согласована).

---

## Обновление: G1 и G3 ПОЧИНЕНЫ (фикс локально, деплой на прод ~18.06 +1.5ч)

Владелец починил G1 (онбординг) и G3 (рассылка напоминаний) локально; деплой на боевой — в течение ~1.5ч. ⚠️ **Реальную реализацию прочитать на проде после деплоя — не гадать** (поля/шаги онбординга, как подключён крон, требует ли самозапись подтверждения email).

**1. G1 — оба пути онбординга** (самозапись + админ-создание). Следствия:
- **Новый тест-пункт «Онбординг»:** проверить оба пути заведения клиента (клиент-видимый флоу): самозапись через `register` (создаёт новый тенант) + админ-создание тенанта.
- **Провижининг 20 клиентов — через реальный онбординг** (заодно тест), **P1 SQL — fallback**. ⚠️ Если самозапись требует подтверждения email — 20 клиентов через неё упрутся в 2 живых ящика (kdv1@bk.ru, stels_info@bk.ru) → часть заведём админ-созданием/SQL.

**2. G3 — пункт 6.** ⚠️ Этот прогноз 18.06 («проверить, что рассылаются») **НЕ сбылся.** По факту деплоя 19.06 напоминания **убраны из портала целиком** (контроллер/команда/крон/таблица/UI удалены). Каноничная формулировка — в пункте 6 выше (карточка REM-GONE): убедиться, что напоминаний НЕТ.

**Проверить на проде после деплоя:**
- точные поля/шаги обоих путей онбординга + требует ли самозапись подтверждения email;
- как подключён `reminders:dispatch-due` в `console.php` (период) и что рассылка реально уходит;
- обновить пункты 6, «Онбординг» и провижининг по факту.

**Статус блокеров (обновлено 19.06):** G1 ✅ **на боевом** (самозапись; админ-создания тенанта в продукте по-прежнему НЕТ — заведение админом = SQL/seed) · G2 ✅ **на боевом** (дайджест, вкл. по умолчанию) · G3 ✅ **на боевом** (напоминания удалены целиком). Деплой `01a9029c` (schema v8.49) выкачен. Открытых блокеров продажи нет (кроме G5 пополнение — ждёт ООО). Тарифы ≤65₽ — НЕ применены (на боевом 500→250₽), отдельная работа.

---

## ПЕРЕСМОТР (18.06, по итогам проработки) — каноничная версия плана

Этот блок — итог детальной проработки; при расхождении с пунктами выше приоритет здесь. Исполняемая форма — ранбуки `docs/superpowers/runbooks/2026-06-18-acceptance-R0..R5`.

### Метод покрытия — all-pairs
- **Раздача** проверяется **all-pairs** (попарное покрытие), а не полным декартом (7 200) и не «по одной ветке».
- **27 прогонов** покрывают все пары S×R×B×Lim×D (доказано генератором: 111 пар, все). Шеринг (N) вынесен в отдельный кластер.
- Дименсии: S {сайт/звонок/смс+слово/смс/DIRECT} · R {Москва82/Москобл56-не-маппится/мульти/всяРФ/непокрытый→подмена} · B {ok/граница/ноль-заморозка} · Lim {обычный/малый} · D {дни вкл/выкл}.
- Коды регионов — **порядковые 1..89** (Москва=**82**, не 77!).
- + **кластер шеринга** (~4: CAP/второй круг на 7 клиентах) + **групповые** (~6: Σ/3-vs-max, регионы/дни-union, всяРФ-перебивает, субдомен→корень). ≈ **37 корректность-прогонов**.

### Популяция (точно)
- **7 клиентов · 16 проектов · 12 rt у поставщика.** Источники: site (C1–C7) + субдомен (C1) + звонок (C1,C2) + смс+слово (C1,C2) + смс (C1,C2) + DIRECT (C1,C2).
- rt у поставщика = по источнику×площадки: site 3 · субдомен 3 · звонок 3 · смс+слово 2 · смс 1 · DIRECT 0 = **12** (чистить до 21:00).
- 37 прогонов идут на 16 проектах через **переконфиг** (~27 пере-слепков + ~13 правок баланса; регион — телефоном лида). Размен «16 проектов/27 слепков ↔ ~30 проектов/10 слепков».
- **Проекты создавать через ПОРТАЛ** (иначе нет синка к поставщику).

### Слой D — пересмотрен (железо слабое)
- Прод = **2 ГБ RAM · 2 vCPU @ 20% доля (burstable) · 20 ГБ**. → не «бёрст 3000», а **адаптивный плавный ramp** с экстраполяцией.
- **Грузим на распределение** (лид → 3 из 7, алгоритм работает по-полной), не на одного.
- **Инструментируем весь прогон** (синк/слепок/инъекция = замер) + **адаптивный ramp до ~3000 лидов** (темп ↑ по факту) + **параллельные правки проектов** (совмещённый случай: воркер+БД+канал) + **safety-stop** (боевая коробка, lkomega).
- **Наблюдение распределения** — 500–1000 лидов спокойно (гистограмма раздачи/списаний/блоков).
- Выход: **кривая темп→нагрузка → прогноз лидов/сутки + сигнал по железу**. Ориентир, не точное.

### Блокировка (§21 карты) — учитывать в B-сценариях
`required` для заморозки/preflight — **share-aware** (доля в заказе группы `⌈groupOrder × лимит/Σ⌉`, не сырой лимит). Шерящий замораживается труднее. Ожидаемые блоки считать по этой формуле vs `capacity(баланс→лиды по ступеням)`.

### G-находки (приедут с деплоем)
- G1 онбординг — оба пути (самозапись: почта+пароль+капча+код+ИНН/DaData+старт 300₽; админ-создание) → тест + (опц.) провижининг через онбординг.
- G2 — письмо о лидах = **дайджест** (раз в 30 мин) → пункт 5 переписать; узкое место H1 (синхронное письмо) возможно ушло — перепроверить.
- G3 — **напоминания УБРАНЫ** → пункт 6 = убедиться, что убраны.
- G4 push убран; **тарифы → ≤65₽** (числа из живой `pricing_tiers`).

### Набор ранбуков (исполняемая форма)
R0 старт · R1 провижининг 7/16 · R2 инъекция · R3 движок+all-pairs+блокировка · R4 слой D (ramp/safety/железо) · R5 финал/teardown. **R3b** (онбординг/дайджест/убр.напоминания) — после деплоя.

### Открытые мелочи
- Популяция 16/27-слепков vs ~30/10-слепков — на усмотрение исполнителя.
- Нагрузочный источник 7 (хватает) vs 10–15 (тяжелее).
- R3b + перечитать G1/G2/G3 на проде после деплоя.
