F-CORPUS: ключевые документы приёмки liderra.ru лежали untracked — мастер- хэндофф ссылался на отсутствующие в git файлы (битые ссылки в новом клоне). Закоммичены: R0–R5 + stepbystep ранбуки, хартия, prod-logic-map, эфир-хэндофф, imitation-checks-table, live-demo/ (эфир-плеер) + смежные specs/планы серий f1-card/phase1/televizor/g1/g2 (решение владельца — «корпус + смежные»). F-DELPROJ: пункт №15 checks-table → «удаление проекта со сделками запрещено (422), сделки целы» (было неточно «сделки сохранены», сверено по ProjectService::delete). Реестр находок: статусы F-DEPTRAC/F-CSV/F-REMIND/F-DELPROJ/F-CORPUS → закрыто. .gitleaks.toml: ранбуки приёмки добавлены в allowlist (синтетические тест- телефоны, та же категория что plans/specs/audits). live-demo HTML: stylelint --fix (#fff→#ffffff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
54 KiB
Приёмочный тест боевого liderra.ru — рабочая хартия (фиксируем по пунктам)
🔄 19.06.2026 — форма показа согласована: приёмка проводится как «телевизор-доказательство» (всё глазами, реальные экраны, два слоя: пошаговая корректность с GO владельца + нагрузка), дизайн — 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-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 (детально — выполняется в конце сессии)
Порядок строгий: сначала чистим у поставщика, потом БД.
- rt-проекты у поставщика: удаление тест-проекта в портале → авто-джоб
DeleteSupplierProjectJobчистит rt-проект на crm.bp-gr.ru; либо админ-инструмент bulk-delete спискаsupplier_projects. Проверить глазами, что тест-rt-проекты исчезли у поставщика. - Слепки инъекций: удалить строки слепка за тест-даты (
snapshot:rebuildза дату или прямой DELETE по тест-проектам, owner-escape). - Тест-тенанты и их данные: SQL-скрипт DELETE по маркеру
TEST-с каскадом по зависимым таблицам тенанта (deals/projects/project_supplier_links/snapshots/lead_charges/balance_transactions/supplier_lead_costs/notifications/reminders/report_jobs и пр.). Скрипт удаления готовится вместе со скриптом провижининга (парный, обратимый). - Сессии/кэш: при необходимости почистить тест-ключи в Redis (throttle входа, dedup вебхука).
- Проверка: после teardown — read-only SELECT, что тест-тенантов и тест-проектов в БД не осталось, у поставщика тест-rt-проектов нет.
Полный перечень функций клиента (что тестируем — ✅ клиентское)
- Вход + 2FA + сброс пароля
- Проекты: создать / править / удалить / массовые / проверка баланса
- Деньги: лента денег + выгрузка + калькулятор «хватит на…»
- Сделки: лента / карточка / правка / ручное создание / статусы / массовые / экспорт
- Колокольчик + email о новом лиде
- Напоминания (создание работает, рассылка ⛔ G3)
- Отчёты (4 типа, выгрузки)
- Импорт сделок из CSV
- Получает письма о заморозке/нуле баланса (фон шлёт, клиент видит)
- Видит только своё (изоляция — проверяется его глазами)
Заведомо сломанное (НЕ тестируем, знаем): 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 (глубокое чтение движка 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. Метод: сверка «экран клиента ↔ боевая БД» (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. Провенанс/отображение (берём ⭐):
- ⭐ источник в ленте
rubvscsv_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. Главный экран продажника + механизм раздачи лида в сделки. Гоняется на реальных сделках из инъекции пунктов 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:
- Дефолт new_lead теперь
{inapp:true, push:true, email:true}— письмо ВКЛЮЧЕНО по умолчанию (G2-B флипнул дефолт; провереноcolumn_defaultusers.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 карточка 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. Пять писем баланса. Гоняем на c19 (малый баланс), письма на stels_info@bk.ru. Триггерим командами вручную + бэкдейт frozen_by_balance_at для окон reminder/final (owner-escape).
Два триггера:
- ⭐ Мгновенный (в доставке): баланс < цены → откат + автопауза +
ZeroBalancePausedMail(1/час). - ⭐ Вечерний sweep @18:00 (
BillingPreflightSweepCommand):requiredLeadsForTomorrowvs ёмкость — смотрит на завтра, может заморозить и с положительным балансом. Не хватает → 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 на проде после деплоя.