Files
portal/docs/superpowers/specs/2026-06-18-acceptance-test-charter.md
T
Дмитрий 3ba703d9c9 docs(приёмка): корпус приёмочного теста + поправка №15 + статусы реестра
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>
2026-06-21 04:48:50 +03:00

54 KiB
Raw Blame History

Приёмочный тест боевого 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 (детально — выполняется в конце сессии)

Порядок строгий: сначала чистим у поставщика, потом БД.

  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 (глубокое чтение движка 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 тест-клиентов:

Клиенты Источник / тип Регионы Лимиты Назначение
c1c7 (7) сайт-1 все Москва по 10 шеринг + «второй круг» + заказ (⌈70/3⌉=24 → B1/B2/B3 по 8); c1 почта kdv1@bk.ru
c8c12 (5) сайт-2 Москва / СПб / Казань / вся РФ / непокрытый 20 каскад фаз 1→2→3
c13 (1) сайт-3 Московская область 10 граница регионов (гео-фильтр теряется)
c14c17 (4) звонок / смс+слово / смс / DIRECT Москва 10 типы сигнала, площадки, DIRECT без pivot
c18c19 (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. Провенанс/отображение (берём ):

  • источник в ленте 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. Главный экран продажника + механизм раздачи лида в сделки. Гоняется на реальных сделках из инъекции пунктов 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_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 карточка 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): 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 2448ч / 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 на проде после деплоя.