Files
portal/docs/Analiz_originala_v8_3.md
T
Дмитрий 887abf444e rebrand(v8.5→Лидерра): дизайн-handoff Платона v8 Forest + Лидпоток→Лидерра
Получен handoff-пакет liderra_v8_handoff/ от дизайнера Платона
(kpd9363@gmail.com) от 07.05.2026 — v8 Forest. Заказчик 08.05 решил
применить только в части дизайна, имени, логотипа. Функционал, состав
страниц и правила (CTO-11, click-wrap, SSO break-glass, 14 статусов
воронки) — без изменений (источник — ТЗ v8.5/schema v8.5).

Что сделано:

- Массовая замена Лидпоток→Лидерра (с учётом падежей: Лидерры/Лидерре)
  в 33 файлах (449 вхождений) — все .md/.sql/.json/.toml/.yml/.txt/.html,
  кроме исторических упоминаний внутри liderra_v8_handoff/
- Удалён docs/brandbook.md v1.1 — заменён на BRANDBOOK_v2.md из handoff
- Скопированы 13 концептов liderra_v8_handoff/concepts/v8_*.html в
  web/v8/. Удалены старые web/01-login.html, 02-dashboard.html,
  03-deals.html, index.html (палитра v1.1 deprecated)
- CLAUDE.md v1.0→v1.1: §0 (BRANDBOOK_v2 + DEVELOPER_HANDOFF в источниках),
  §2 (палитра Forest, Inter+JBM, Lucide), §5 п.6 (anti-pattern Inter
  снят — в Forest Inter наш основной шрифт), §6 (13 концептов в web/v8/)
- Реестр Открытые_вопросы_v8_3.md v1.12→v1.13: добавлена запись о
  ребрендинге + 4 точечных расхождений handoff vs ТЗ (статусы воронки,
  click-wrap чекбоксы, SSO fallback, axe violations)
- package.json/package-lock.json: name lidpotok→liderra

4 расхождения handoff vs ТЗ (НЕ применены, источник истины — ТЗ/schema):

1. 14 «обобщённых» статусов в BRANDBOOK_v2 §3.6 ≠ 14 slug'ов в
   schema.sql:2076 (совпадает 2 из 14: «Переговоры», «Оплачено»).
   Источник — schema/ТЗ §6.4 (реселлерская модель из аудита crm.bp-gr.ru,
   6 системных + 8 настраиваемых статусов).
2. 3-й click-wrap в v8_login.html («маркетинг-опционально») ≠ ТЗ §1.5/§4.1
   («согласие на ПДн», обязательное, OPEN-Ж-3).
3. SSO в v8_admin.html («локальный 2FA fallback») ≠ ТЗ OPEN-И-13
   (break-glass super_admin, локальный 2FA выключен).
4. Заявление «axe-core 4.10.2 — 0 violations» в README handoff — локально
   Pa11y 9.1.1 + axe нашёл 81 violation на 10/13 HTML (преимущественно
   color-contrast на декоративных separator'ах с --ink-disabled).
   Чисто: settings/errors/palette_options.

Что НЕ включено в коммит:
- лендинг/TZ_landing_v1_0.md — untracked, не моя работа в этой сессии
- .tmp/ — gitignored

Что осталось (для следующих сессий):
- Возможное переименование GitHub-репо CoralMinister/lidpotok → liderra
  (отдельное решение заказчика)
- Опционально: обратная связь Платону по 4 расхождениям handoff vs ТЗ

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:11:58 +03:00

130 KiB
Raw Blame History

Приложение М — Анализ оригинала crm.bp-gr.ru и архитектурные следствия для Лидерры (v8.3)

Дата: 05.05.2026 (расширено по итогам партий 12–15). Версия: 1.1 (расширено по итогам параллельного аудита партий 12–15 от 05.05.2026). Адресат: заказчик + CTO + backend + frontend + продакт. Источники данных:

  • crm-bp-gr-audit-2026-05-04.md — партии 1–11 (исходный аудит, не входит в архив v8.3++ optimized; хранится в предыдущей итерации).
  • audit-batch-12-2026-05-05.md — Конверсия проектов + Напоминание + Досье.
  • audit-batch-13-2026-05-05.md — Рекомендации источников + Настройки реестра + Просмотреть + Список звонков.
  • audit-batch-14-2026-05-05.md — Редкие статусы + База знаний + Безопасность профиля.
  • audit-batch-15-2026-05-05.md — Карточки внешних интеграций «API ▾».

⚠ Состояние на 05.05.2026 (v8.3++ optimized): 4 файла audit-batch-12..15-2026-05-05.md объединены в единый документ Аудит_partii_12_15_originala_v8_3.md (части 12, 13, 14, 15). Содержимое полностью сохранено без изменений. Все ссылки в данном Прил. М вида «партия 12.2.4», «партия 15.3» и т. п. читать с учётом нового размещения: соответствующая «Часть N, раздел X.Y» в Аудит_partii_12_15_originala_v8_3.md.

Назначение: перевести наблюдения аудита в формальные архитектурные решения для платформы Лидерра. Закрыть пробелы между нашей моделью v8.3 и реальной структурой оригинала. Поднять перед заказчиком новые продуктовые вопросы, возникшие из аудита.

История изменений:

  • v1.0 (04.05.2026) — исходный анализ по 11 партиям, 4 новых вопроса (Биз-10/11/12/13).
  • v1.1 (05.05.2026) — добавлены результаты партий 12–15 (раздел 9), 3 новых вопроса (Биз-14/15/16), уточнения в §3 (новая модель reminders, расширение suppliers capabilities, новое поле tenants.desired_daily_numbers — §3.5), уточнение в §2.5 (Биз-10 переоткрыт), окончательный вердикт по outbound webhooks (§2.3).

0. TL;DR

Аудит crm.bp-gr.ru, проведённый в 15 партий за 04.0505.05.2026 (11 исходных + 4 параллельные), подтвердил архитектурные решения Лидерры, обнаружил расхождения и выявил новые продуктовые вопросы.

Главное расхождение №1: в оригинале существует архитектурный слой «Поставщик» (B1/B2/B3) с разными capabilities для каждого (B1 — звонки/сайты, B2 — sms+sender_name+keyword, B3 — sms+только sender_name). У нас этого слоя не было — нужна таблица suppliers с полями channel, supports_sender_name, supports_keyword + связь project_suppliers.

Главное расхождение №2: в оригинале дневной лимит проекта автоматически режется при нехватке баланса (effective_daily_limit = min(project.daily_limit, balance / lead_cost)) — это бизнес-правило, без которого продуктовый паритет с оригиналом не достигается. У нас сейчас лимиты статичные.

Главное расхождение №3 (новое в v1.1): в оригинале множественные напоминания на сделку реализованы через массив histories[].type='reminder', не через одиночное поле. У нас в schema v8.2 — поля deals.reminder_text + deals.reminder_at (одиночное). Нужна отдельная таблица reminders с FK на deals (поля минимального паритета: text + remind_at + created_by + created_at).

Главное подтверждение: в оригинале исходящих webhook'ов нет — окончательно, 7 независимых линий доказательств (партии 10 + 15). Уточнение: для Bitrix24 есть один hardcoded URL prostats.info/bitrix24/webhook.php, но это inbound-канал (Bitrix → prostats → CRM), не outbound CRM. Наш Уровень 1 (outbound webhook на MVP, OPEN-И-2) — это конкурентное преимущество.

Безопасность оригинала (новое в v1.1):

  • В DOM каждой страницы — подтверждённая prompt injection-атака с явным таргетингом на агентов-Claude (партия 10, повторно подтверждено в 12/13/14/15). Источник не установлен. Действия Claude в Chrome корректные во всех 4 партиях.
  • На странице профиля пароль пользователя виден в HTML-форме plaintext (<input type="text" name="User[password]"> — партия 14). У нас выделенная защищённая форма «старый/новый/подтверждение».
  • В карточках интеграций API-ключи и client_secret хранятся как <input type="text"> (Скорозвон, Unisender, Мои Звонки — партия 15). У нас должны быть type="password" + кнопка «показать».
  • В оригинале отсутствуют 2FA, журнал входов, список сессий, история паролей (партия 14). У нас всё это есть в schema v8.2 (auth_log, user_sessions, user_recovery_codes) — это сильное конкурентное преимущество.

3 новых продуктовых вопроса (Биз-14/15/16): TTL удаления проектов, wizard рекомендаций источников, таргет desired_daily_numbers. Все с дефолтами, не блокеры. См. §9.5.


1. Контекст аудита

1.1. Что было сделано

Аудит провёл оператор Claude в Chrome в read-only режиме (без сохранений, удалений, изменений настроек). Аккаунт владельца с ролью админа. ПДн клиентов замаскированы.

Аудит проведён в 2 захода — всего 15 партий:

Заход 1 — 04.05.2026 (11 партий)

Раздел Что исследовано Заметка
1 Каркас приложения Меню, шапка, дашборд, реестр сделок, реестр проектов Render error в первой версии 1.4 — переписан в 11
2 Сделка и статусы Карточка сделки, 14 статусов Переписан в партии 2-bis (11)
3 API / Интеграции Ingress endpoint, документация
4 Воронки и статусы Персонализация воронок, kanban
5 Биллинг и тарифы История транзакций, баланс Заглушка «Технические работы» — не доступно
6 Профиль, безопасность, пользователи Профиль, RBAC, magic-link
7 Аналитика и звонки Отчёты, конверсия проектов, звонки
8 База знаний и задачи Внешний ресурс / нет в меню
9 Технический срез Стек, console, network Vue 2 + Vuetify 1.5 + Element UI + Yii2
10 Карточка проекта (повторный заход) 3 типа карточек + форма создания + история действий за 5 месяцев Раскрыта сущность «Поставщик»
11 Карточка сделки (повторный заход) 5 реальных сделок в 5 разных статусах Подтверждена свободная state-machine

Заход 2 — 05.05.2026 (4 партии)

Раздел Что исследовано Заметка
12 Конверсия проектов + Напоминание + Досье Структура отчёта 17 колонок, форма напоминания (4 поля), 2 разные модалки «Досье» Биз-10 переоткрыт — модель histories[] с массивом напоминаний
13 Рекомендации источников + Настройки реестра + Просмотреть + Список звонков Wizard OSINT-разведки, аккордеон настроек, иконка «Просмотреть», агрегация звонков B1/B2/B3 жёстко подтверждены — цитата из UI о capabilities; новое поле desired_daily_numbers
14 Редкие статусы + KB + Безопасность профиля Косвенное подтверждение единой карточки во всех 14 статусах, внешняя KB HelpDeskEddy, /admin/user/account Ключевое: в оригинале нет 2FA, нет auth_log, нет sessions — наше преимущество. Анти-паттерн: пароль plaintext в HTML.
15 Карточки внешних интеграций «API ▾» 12 пунктов меню, 5 открытых карточек (Bitrix, amoCRM, Скорозвон, Unisender, Мои Звонки), JS-поиск 31 keyword, network-мониторинг Финальный вердикт: outbound webhook'ов нет (7 линий доказательств). Только hardcoded receiver prostats.info/bitrix24/webhook.php (inbound). Анти-паттерн: API-key в <input type="text">

1.2. Что НЕ исследовано (важно учитывать)

После 15 партий пробелов осталось значительно меньше. Закрыты партиями 12–15: форма «Напоминания» (партия 12), модалка «Досье» (партия 12), внешние интеграции «API ▾» (партия 15), безопасность профиля (партия 14). Косвенно подтверждены — единая структура карточки во всех 14 статусах (партия 14, через DOM фильтра).

Остающиеся пробелы:

  1. Биллинг полностью/admin/site/pay-history и /admin/site/balance отдают «Технические работы». Структуру транзакций, тарифной сетки, формы пополнения восстановить невозможно. Следствие: все наши решения по биллингу (§20–21) — не «реверс-инжиниринг», а наша собственная модель с оглядкой на видимый виджет «4 счётчика в шапке».
  2. Эмпирическая проверка карточки сделки в 9 редких статусах (Новый, Оплачено, Горячий, На замену, Партнёрка, Ожидаем оплаты, Конечный недозвон, Тест драйв, База) — на текущем аккаунте 0 сделок во всех 14 статусах. Косвенное подтверждение единой структуры через DOM фильтра + партию 11 для 5 «частых» статусов. Окончательная эмпирическая проверка требует аккаунта с непустыми редкими статусами.
  3. Tab «Pr-cy» в модалке visitdop — частично заблокирован render-fn, требует сделки с заполненным доп-разделом (model.isVisitDop=true). На MVP не реализуется (см. §9.1.4), поэтому пробел не критичен.
  4. Голосовой Робот (3 sub-страницы), Google Sheets, Chat GPT — карточки не открыты в партии 15 как «низкоприоритетные для гипотезы outbound webhook». HTML серверным fetch проверен (0 webhook-keyword), что исключает основную гипотезу.
  5. Реальный TTL удалённых проектов — нижняя граница ≥3 месяца (наблюдение 96 дней), верхняя не определяется без админ-доступа. Требует решения Биз-14.

1.3. Технические условия аудита

  • Outbound webhook поиск: проверены 24 keyword'а (webhook, postback, колбэк, s2s, server-to-server, endpoint, ingress, вебхук, пиксел, signature, hmac, bearer, retry, timeout, payload, тестовая отправка и др.) — ноль совпадений в карточке проекта, форме создания, истории за 5 месяцев и 27 уникальных дат.
  • Real-time: WebSocket / SSE — 0 каналов на всех исследованных страницах.
  • XHR: 6 эндпоинтов в партии 10, все внутренние GET. Списки грузятся POST + CSRF.
  • Render error: TypeError: Cannot read properties of undefined (reading '0') в Element UI повторяется 4× за каждую навигацию. Сломал отрисовку таблицы сделок в первой сессии — отсюда ошибочный вывод «0 сделок». На самом деле в системе 12 876 сделок (выборка visit=rt).

2. Главные архитектурные открытия

2.1. 🔴 Поставщики B1/B2/B3 — отдельная сущность над проектом

Что было неясно до партии 10. Префиксы B1_, B2_, B3_ фигурировали в slug'ах статусов и тегах проектов с самого начала аудита, но смысл был непонятен. Гипотеза до аудита: «это просто префиксы названий».

Что раскрыла партия 10. B1/B2/B3 — это три внутренних поставщика данных (источников номеров) в оригинале, между которыми проект может выбирать:

  • B1 — основной поставщик для типов «Сайты» и «Звонки».
  • B2 — поддерживает «СМС» через связку «Наименование отправителя» + «Ключевое слово».
  • B3 — поддерживает «СМС» только по наименованию отправителя.

Где видно. В форме создания нового проекта B1/B2/B3 — это три чекбокса (все checked по умолчанию). В карточке существующего проекта — readonly-индикаторы (см. партию 10.3, секция 3 структуры карточки).

Что это означает для модели данных.

Реальная цепочка не «Проект → Лид», а «Поставщик → Проект → Лид», где у каждого поставщика свои правила приёма (тип данных, обязательные поля, формат payload).

Чего у нас сейчас нет (schema.sql v8.1):

  • Нет таблицы suppliers.
  • В supplier_lead_costs поле supplier_code VARCHAR(50) со значением по умолчанию 'crm_bp_gr' — то есть один абстрактный поставщик для всей платформы.
  • В system_settings константа supplier_default_cost_rub — одна цена для всех лидов.
  • В projects нет связи с поставщиком вообще.

Что нужно добавить (детально — §3.1). Новая таблица suppliers (B1/B2/B3 как seed, расширяемо), связь m2m project_suppliers с per-supplier настройками, миграция supplier_lead_costs.supplier_codesupplier_id BIGINT REFERENCES suppliers(id).

Замечание. Заказчик в реселлерской модели (Ю-2) покупает у crm.bp-gr.ru как у одного контрагента — то есть для нас поставщик первого уровня = crm_bp_gr, а B1/B2/B3 — это «суб-поставщики» в рамках crm.bp-gr.ru. Это важно для DDL: suppliers.parent_id или отдельный уровень supplier_channels. Решение в §3.1.

2.2. 🔴 Динамическая регулировка лимитов по балансу

Что зафиксировано в партии 10.6. В истории действий проекта (/admin/visit/rt-clogs-load?id=<project_id>) обнаружена системная запись:

Проект был запущен с лимитом N по причине нехватки баланса. По факту M

Это означает, что в оригинале есть джоба планирования, которая ежедневно (или при пополнении/списании) пересчитывает реальный лимит проекта по формуле:

effective_daily_limit = min(project.daily_limit, available_balance / lead_cost)

Что у нас сейчас. В schema.sql:

  • В projects нет полей daily_limit или effective_daily_limit.
  • Нет джобы планирования лимитов.
  • Бизнес-логика автокоррекции не описана в narrative.

Что нужно добавить (детально — §3.2). Поля projects.daily_limit_target (что хочет клиент) и projects.effective_daily_limit_today (что реально на сегодня — кэш). Сервис EffectiveLimitCalculator, вызываемый при пополнении баланса, при создании проекта, и cron-ом в 00:00 МСК. Логирование автокоррекции в activity_log с типом auto_limit_adjustment.

2.3. 🟢 Подтверждено: исходящих webhook'ов в оригинале НЕТ (окончательно)

Что было предположено до партии 10. «Возможно, оригинал умеет отправлять лиды наружу через webhook — проверить надо отдельно».

Что подтвердила партия 10 (заход 1). Сильное «нет», по 6 независимым линиям доказательств. Оставался единственный пробел — карточки в шапке «API ▾».

Что закрыла партия 15 (заход 2). Окончательное подтверждение — карточки 5 интеграций (Bitrix24, amoCRM, Скорозвон, Unisender, Мои Звонки) из 12 пунктов меню детально открыты, плюс серверный HTML-fetch ещё 4 страниц. Ни в одной нет редактируемого поля «Webhook URL»/«Callback URL»/«Notify URL». Поиск по 31 webhook-keyword в 71 КБ кастомного JS — 0 совпадений. Network-мониторинг — 0 внешних XHR, все вызовы провайдер-API идут серверной стороной.

Полный список доказательств (7 линий):

  1. 3 типа карточек проекта (Сайты / Звонки / СМС) — нет полей webhook (партия 10).
  2. Форма создания нового проекта — нет полей webhook (партия 10).
  3. История действий проекта за 5 месяцев и 27 уникальных дат изменений — нет логов webhook-параметров (партия 10).
  4. DevTools Network — 6 XHR за сессию, все внутренние GET (партия 10).
  5. WebSocket / SSE — 0 каналов (партия 9).
  6. Поиск по 24 webhook-keywords в HTML — ноль совпадений (партия 10).
  7. 5 открытых карточек интеграций + поиск 31 keyword в JS + network-мониторинг — 0 (партия 15).

Уточнение по партии 15. Найден один hardcoded URL prostats.info/bitrix24/webhook.php?id={{formData.id}} в Vue-template карточки Bitrix24 — это read-only текст для копирования пользователем в админку Bitrix24. Архитектурно это inbound-канал для CRM: Bitrix24 постит туда события, prostats.info принимает и передаёт в crm.bp-gr.ru через серверную связь. То есть с точки зрения CRM это endpoint для приёма, не для отправки — что не противоречит выводу «исходящих webhook'ов нет».

Природа интеграций оригинала. В партии 15 структурно изучены все 5 открытых карточек:

  • CRM (Bitrix24, amoCRM): outbound API-clients с токеном/secret. CRM пушит лиды в Bitrix/amoCRM по REST API.
  • Телефония (Скорозвон, Мои Звонки, Mango): outbound API-clients с тогглами «Передавать визиты» / «Передавать сделки». Ключевые поля — client_id+client_secret+api_key (OAuth2 client credentials или basic auth). URL получателя НЕ настраивается — захардкожен в backend.
  • Email (Unisender): outbound API-client с токеном.

То есть архитектурный паттерн оригинала — провайдер-специфичные backend-интеграции с переключателями ON/OFF, но без generic «push в любой webhook URL по выбору пользователя».

Что это означает для нас. Решение OPEN-И-2 «Уровень 1 (outbound webhook) на MVP + amoCRM-коннектор в спринте 14–15» — это двойное конкурентное преимущество:

  1. Generic webhook subsystem (URL+secret+events+retry+test) — то, чего у оригинала категорически нет. У нас должно быть в продуктовой документации с явным указанием.
  2. amoCRM-коннектор — паритет с одной из 12 интеграций оригинала, на которой клиенты завязаны. По полям (Субдомен, API-ключ, маппинг полей контакта/сделки/задачи) — структура из партии 15.2.2 — может быть прямым ориентиром.

2.4. 🔴 Карточка сделки всегда editable, нет state-machine переходов

Что зафиксировано в партии 11. Из 5 проверенных сделок в 5 разных статусах (Проработан, Недозвон, Просмотрено, Закрыто и не реализовано, Переговоры):

  • Все поля guest-block editable, включая статус «Закрыто и не реализовано».
  • Селектор статуса в любой сделке показывает все 14 значений — никаких ограничений «из A нельзя в B».
  • Структура карточки полностью идентична для всех 5 статусов — нет условных полей или условных действий.
  • Возврат из терминальных статусов (Закрыто, Конечный недозвон) разрешён.

Что у нас в narrative v8.3. В §8 (Воронка продаж и статусы) явных запретов на переходы нет, но и явного разрешения «из любого в любой» — тоже. Это пробел: разработчик может реализовать ограничения «по интуиции», что разойдётся с оригиналом.

Что нужно добавить. Явная фиксация в §8: «переходы статусов сделки — свободные, без машины состояний; возврат из терминальных статусов разрешён». В Прил. В (State machines) — отдельный пункт «состояний deal'ов state-machine НЕТ — паритет с оригиналом».

2.5. 🔴 «Напоминания» — массив на сделку, минимальный паритет — отдельная таблица (Биз-10 уточнён)

Что зафиксировано в партии 11.6. В карточке сделки нет ни секции задач, ни вкладки, ни кнопки «Создать задачу», ни полей ответственного/срока/типа/приоритета. При этом в фильтрах списка сделок (партия 1.4) задачи фигурируют («Дела на сегодня», «Просроченные», «Предстоящие», «Сделки без задач»).

Гипотеза партии 11: карточка «Напоминание» в левой колонке — это и есть «задача». Модель «1 напоминание = 1 deal», а не «массив задач».

Что выяснила партия 12 (детальный аудит формы напоминания) — гипотеза переоткрыта:

В оригинале реальная модель — массив напоминаний на сделку:

  • В Vue-data сделки model.histories[] — массив всех событий, где каждое напоминание = отдельная запись {type:'reminder', time, time_dt, id, note, from, to}.
  • В коде model.histories.filter(h => h.type === 'reminder') итерируется в шаблоне.
  • Каждое напоминание удаляется индивидуально по history.id.
  • Иконка + без ограничения «нельзя добавить второе».
  • Эндпоинт POST /admin/visit/visit-vm-delete принимает индивидуальный id записи.

То есть технически множество поддержано полностью. Эмпирически в 5 проверенных сделках было ровно 1 или 0 напоминаний — UX-паттерн «обычно одно», но это не архитектурное ограничение.

Поля минимального паритета (из формы партии 12.2.3):

Поле Vue path Тип Обязательное Что соответствует
Текст addReminder.note textarea, лимит 255 нет text в нашей модели
Дата addReminder.date el-date-picker да компонент remind_at
Час addReminder.hour el-select 0..23 да компонент remind_at
Минуты addReminder.min el-select 0..59 да компонент remind_at
from (автор) history.from login строка автоматически created_by в нашей модели
to (исполнитель) history.to null во всех проверенных задел на assignee_id, не используется

Чего в форме оригинала НЕТ (важно для решения о паритете):

  • Ответственный (выбор менеджера) — поле to объявлено, но в UI редактирования не выведено.
  • Тип/приоритет (low/med/high) — нет.
  • Канал уведомления (email/push/sms) — нет.
  • Повторение (одноразовое/еженедельно) — нет.
  • Привязка к статусу (автосоздание при смене статуса) — нет.

Что у нас сейчас в schema.sql v8.2. В deals есть поля reminder_text TEXT и reminder_at TIMESTAMPTZ, индекс idx_deals_reminder для cron уведомлений. Это не паритет — это упрощение. Нужно мигрировать на отдельную таблицу reminders.

Дашборд задач — бесплатный паритет. В оригинале фильтры списка сделок принимают параметры:

  • ?reminders=today — «Дела на сегодня»
  • ?reminders=last — «Просроченные дела»
  • ?reminders=future — «Предстоящие дела»
  • ?reminders=none — «Сделки без задач»

Это значит, бэкенд оригинала умеет фильтровать deals по агрегатам reminders. У нас на нашем GET /api/deals?reminders=today|last|future|none это закрывается простым WHERE EXISTS subquery — отдельной сущности «дашборд задач» делать не нужно.

Решение для Биз-10 (уточнено):

  • Реализуем минимальный паритет — отдельная таблица reminders с полями: id, deal_id, tenant_id (для RLS), text VARCHAR(255), remind_at TIMESTAMPTZ, created_by FK→users, assignee_id FK→users NULL (задел, не используется в UI), completed_at TIMESTAMPTZ NULL, is_sent BOOLEAN, sent_at TIMESTAMPTZ NULL. Без приоритетов / каналов / recurrence — это можно отложить в Post-MVP.
  • Удаляем поля deals.reminder_text и deals.reminder_at — они теперь в новой таблице.
  • Дашборд задач через существующий /api/deals?reminders=... — паритет через 1 query parameter, без отдельного экрана.
  • 🟡 Биз-10 переоткрыт: заказчик подтверждает «массив напоминаний без приоритетов/каналов» (паритет с оригиналом, рекомендация Claude) или хочет «массив с приоритетами/каналами/исполнителями» (улучшение). Дефолт — паритет.

См. §3.X (новый раздел) — DDL для таблицы reminders.

2.6. 🟡 В карточке сделки нет файлов / вложений

Что зафиксировано в партии 11.6. Ни <input type=file>, ни секции «Документы», ни превью изображений ни в одной из 5 сделок.

Объяснение из аудита: «Лидген-CRM не работает с документооборотом — лид это телефон + статус + история, не сложный pipeline».

Что у нас в narrative. Раздела «вложения к сделке» в §7–11 нет.

Открытый вопрос для нас. Делаем ли мы паритет (вложений нет) или улучшение (вложения есть как Post-MVP)? Низкоприоритетное решение, можно оставить на Post-MVP по умолчанию.

2.7. 🟡 Телефония — внешняя интеграция, не наш модуль

Что зафиксировано в партии 7.3 и 11.5. Записи звонков хранятся не на инфраструктуре crm.bp-gr.ru, а на внешнем CDN провайдера телефонии «Скорозвон» (oki.needcallbuy.ru/skrzvn/audio/YYYY/MM/DD/{recordId}.mp3). CRM хранит только URL и метаданные (направление, оператор, timestamp, длительность через косвенный счётчик «Минуты»).

Кнопок:

  • Встроенный браузерный плеер <audio controls>.
  • Кнопка «Скачать запись» — нет.
  • CTI-кнопка «Позвонить» / click-to-call — нет.

Что у нас в narrative. Телефония не упоминается вообще.

Открытый продуктовый вопрос. Нужна ли нам телефонная интеграция на MVP? Если да — со «Скорозвоном» (использует оригинал, понятная архитектура), MANGO Office (популярнее в РФ, лучше API), Telphin (бюджетный)? Это Биз-12 — см. §5.

2.8. 🟡 Мульти-кошелёк биллинга (4 счётчика в шапке)

Что зафиксировано в партии 1.2. Виджет «Баланс ▾» в шапке оригинала имеет 4 раздельных счётчика:

  • Баланс ГЦК = 195 (лиды Городского Центра Колл)
  • Диалоги КЦ = 154 (диалоги колл-центра)
  • Минуты = 0 (минуты разговоров)
  • Автозвонок = 0 (автозвонок-операции)

Что это означает. В оригинале четыре раздельных «кошелька» под разные операции (приём лидов, обработка диалогов, звонки, автодозвон). Не «один баланс на всё», а отдельные счётчики, скорее всего покупающиеся отдельными пакетами.

Что у нас в schema.sql. В tenants поля balance_rub (рубли) и через balance_transactions ведётся учёт. На уровне «лидов» — отдельная сущность lead_packages, где лид имеет себестоимость в рублях. То есть у нас одновалютный кошелёк (рубли) с учётом конверсии «руб ↔ лид» через тариф.

Открытый продуктовый вопрос. Делаем ли мы паритет (4 раздельных кошелька) или нашу упрощённую модель (1 кошелёк в рублях + конвертация)?

Продуктовая интерпретация: наша модель — упрощение, более привычное для бухгалтерии. Модель оригинала — телеком-специфика, удобная если у клиента действительно разные пакеты под разные операции (например, «купил 1000 диалогов отдельно, не за лиды»). На MVP мы выбрали упрощение — это Биз-11 для подтверждения / опровержения заказчиком (см. §5).

2.9. 🟢 Аудит-пробел оригинала — наше преимущество

Что зафиксировано в партии 11.4. В истории сделки в оригинале НЕ логируются:

  • Смена ответственного (manager_id).
  • Смена проекта (project_id).
  • Изменение телефона (phone).
  • Изменение суммы.

Логируются только: смена статуса, создание/изменение комментария, исходящий звонок.

Что у нас. В §14 narrative «Журнал действий» планируется полный аудит всех мутаций сделки через activity_log.

Маркетинговое позиционирование. Подсветить в §14 и в маркетинговых материалах: «у нас полный аудит всех изменений сделки, в отличие от оригинала, где можно незаметно перевесить лид на другого менеджера».

2.10. 🟢 Нет kanban — наше преимущество

Что зафиксировано в партии 4.3. В оригинале сделки представлены только табличным видом с фильтром по статусу, без drag-and-drop колонок.

Что у нас. §11 (Kanban-доска) — отдельный полноценный экран.

Маркетинг. Усилить позиционирование: «kanban с drag-and-drop, как в Bitrix24/amoCRM/Pipedrive — то, чего нет в оригинале».

2.11. 🟢 Нет real-time (WebSocket/SSE) — наше преимущество

Что зафиксировано в партии 9.3. WebSocket / SSE не обнаружены. Обновления только по таймеру/перезагрузке.

Что у нас. Push-уведомления (§17) и общий план real-time через SSE при росте.

Маркетинг. «Реалтайм-уведомления о новых лидах — мгновенная реакция, без обновления страницы».

Что зафиксировано в партии 6.5. В оригинале админ может сгенерировать одноразовую ссылку входа для менеджера (срок действия 24ч), и менеджер заходит без пароля. Удобно для подрядных колл-центров с большой текучкой.

Что у нас. Impersonation Ю-1 (15 мин код) сделано, но это другой механизм («войти как клиент»). Magic-link для входа в свой собственный аккаунт — не сделано.

Открытый продуктовый вопрос. Делаем ли magic-link 24ч для менеджеров на MVP или Post-MVP? Это Биз-13 — см. §5.

2.13. 🟢 «Тихие часы» в профиле — формат уточнён партией 14

Что зафиксировано в партии 14.3. Полная структура «Тихих часов» в /admin/user/account:

Параметр Значение в оригинале
Поле «от» <select> User[popup_time_start] с 24 опциями (0..23, целые часы)
Поле «до» <select> User[popup_time_end] с 24 опциями (0..23)
Default start=10, end=19
Минимальный интервал 3 часа (server-side validation, hint показывается рядом)
Часовой пояс Отдельное поле User[timezone_id] в блоке «Системная информация» (default «Москва») — общий для всего профиля, не привязан к окну тихих часов
Гранулярность только часы (нет минут, нет дней недели, нет нескольких окон в сутки)
Per-channel настройки нет — одно окно на все каналы (push/popup/звонки)

Что у нас в narrative. В users.notification_preferences JSONB матрица 8×3 каналов (CTO-4), но окно «не беспокоить» там явно не описано. Нужно добавить.

Решение (минимальный паритет):

// users.notification_preferences — расширение
{
  "channels": { /* матрица 8×3, как в CTO-4 */ },
  "sound_enabled": true,
  "quiet_hours": {
    "enabled": true,
    "start_hour": 22,        // целое 0..23 (паритет с оригиналом)
    "end_hour": 8,           // целое 0..23 (паритет)
    "min_interval_hours": 3  // server-side validation, как в оригинале
    // timezone берётся из users.timezone — общий, не привязан к quiet_hours
  }
}

Расширения сверх паритета (не блокируют MVP, но «лучше оригинала»):

  • Точность HH:MM (не только часы) — UI компонент tim picker.
  • Per-channel quiet hours (отдельно для push / email / SMS / WA / TG) — для случая «push можно ночью, SMS нельзя».
  • Дни недели (включено/выключено по дням).
  • Несколько окон в сутки (например, 13:0014:00 обед + 22:0008:00 ночь).

Все 4 расширения — JSONB-совместимые, не требуют изменений schema.sql.

2.14. 🟢 14 фиксированных статусов, 8 переименовываются

Что зафиксировано в партиях 2.2 и 4.1. В оригинале:

  • Системный набор из 14 статусов (slug + русское имя).
  • Переименовывать можно 8 из 14 (через /admin/site/states).
  • CRUD статусов в UI нет.

Что у нас. Таблица lead_statuses (14 строк) + tenant_status_overrides для кастомных имён. Полное соответствие.

2.15. 🔴 B1/B2/B3 — capabilities (новая деталь из партии 13)

Что зафиксировано в партии 13.3.5. В модалке редактирования проекта (та же, что вызывается по «Просмотреть» — это aria-label на pencil-иконку) обнаружен оранжевый notice, цитата:

«Важно! Указать в связке 'Наименование отправителя' и 'Ключевое слово' можно только по поставщику B2. Поставщик B3 работает только по наименованию отправителя, учтите данный момент при формировании задания.»

Что это меняет в модели данных. Поставщики B1/B2/B3 различаются не только каналом (Сайты/Звонки/SMS), но и доступными для проекта полями (capabilities):

Capability B1 B2 B3
Канал (channel) sites + calls sms sms
Поддержка sender_name
Поддержка keyword
Загрузка CSV/XLS списка доменов/телефонов — (sms-канал)
Указание списка доменов

Что нужно расширить в suppliers (по сравнению с §3.1 v1.0):

ALTER TABLE suppliers
    ADD COLUMN channel              VARCHAR(20) NOT NULL DEFAULT 'sites'
        CHECK (channel IN ('sites','calls','sms')),
    ADD COLUMN supports_sender_name BOOLEAN NOT NULL DEFAULT FALSE,
    ADD COLUMN supports_keyword     BOOLEAN NOT NULL DEFAULT FALSE,
    ADD COLUMN supports_csv_upload  BOOLEAN NOT NULL DEFAULT TRUE,
    ADD COLUMN supports_domains_list BOOLEAN NOT NULL DEFAULT TRUE;

Зачем эти поля. Для frontend проекта: какие input'ы рендерить в карточке проекта в зависимости от выбранного поставщика. Без supports_* фронтенду нужно хардкодить «B1=такие поля, B2=другие, B3=третьи» — что ломает абстракцию suppliers.

Seed-данные (после §3.1 + §2.15):

INSERT INTO suppliers (code, name, channel, supports_sender_name, supports_keyword,
                       supports_csv_upload, supports_domains_list, accepts_types, cost_rub)
VALUES
  ('b1', 'B1 — Сайты и Звонки',         'sites', FALSE, FALSE, TRUE, TRUE,
   ARRAY['websites','calls'], 1.00),
  ('b2', 'B2 — СМС с ключевым словом',  'sms',   TRUE,  TRUE,  FALSE, FALSE,
   ARRAY['sms'], 1.00),
  ('b3', 'B3 — СМС по наименованию',    'sms',   TRUE,  FALSE, FALSE, FALSE,
   ARRAY['sms'], 1.00);

DDL-патч уйдёт в schema.sql v8.3 (§3.1 этого Прил. М).

2.16. 🔴 «Желаемое кол-во номеров в день» — аккаунтное поле (новое из партии 13)

Что зафиксировано в партии 13.2.2. В аккордеоне «Настройки» в шапке реестра проектов есть кнопка «Желаемое кол-во номеров», открывающая модалку с одним числовым полем. Цитата текста модалки:

«Дорогой клиент, пожалуйста укажите сколько номеров в день вы готовы принимать: на это значение будет ориентироваться сервис ГЦК и в случае если желаемое кол-во не достигается, наши специалисты будут помогать в достижении указанного показателя.»

Что это значит. Это аккаунтный таргет, не лимит. Не массовая установка daily_limit_target по проектам, не влияет напрямую на effective_daily_limit_today. Это ориентирующее значение для бэк-офиса ГЦК (для людей-операторов поставщика), которые ручным трудом дотягивают объём до целевого.

Что у нас сейчас. Этого поля нет.

DDL для schema.sql v8.3:

ALTER TABLE tenants
    ADD COLUMN desired_daily_numbers INT NULL
        CHECK (desired_daily_numbers IS NULL OR desired_daily_numbers > 0);

COMMENT ON COLUMN tenants.desired_daily_numbers IS
    'Желаемое количество номеров в день — целевой ориентир для саппорта/бэк-офиса. '
    'NULL = клиент не задал значение. Не является лимитом, не влияет на '
    'effective_daily_limit. Используется в аналитике "недополучаем ли клиент свой целевой объём" '
    'для проактивной работы саппорта. Партия 13.2.2 аудита — паритет с оригиналом.';

Биз-16: аккаунтное поле desired_daily_numbers — оставляем (полезный сигнал для саппорта). Дефолт: делаем.

2.17. 🔴 Soft-delete проектов с длинным TTL (новое из партии 13)

Что зафиксировано в партии 13.2.3. Отдельный экран «Удалённые проекты ГЦК» (/admin/visit/rt?type=deleted):

  • Идентичен по структуре основному реестру, строки с розовой подсветкой.
  • Дополнительная колонка «Дата удаления» (формат YYYY-MM-DD HH:MM:SS).
  • Наблюдаемые данные: проекты, удалённые 2026-01-29 06:29:54, на момент аудита (2026-05-05) хранятся 96 дней (≈3.2 месяца).

Что у нас сейчас в schema.sql v8.2. В projects есть deleted_at TIMESTAMPTZ, но политика retention не описана — нет cron, нет константы TTL, нет force_delete.

Что добавить (Биз-14):

  1. Новый ключ в system_settings:
INSERT INTO system_settings (key, value, description) VALUES
  ('projects_purge_deleted_enabled', 'false',
   'Включить cron задачу удаления проектов после soft-delete TTL. По умолчанию выключено.'),
  ('projects_purge_deleted_ttl_days', '180',
   'TTL soft-delete проектов в днях (минимальный паритет с оригиналом — нижняя граница ≥3 мес = 90 дней; default 180 = 6 мес для безопасности).'),
  ('projects_purge_deleted_cron', '0 4 * * *',
   'Cron-расписание задачи projects:purge-deleted (default 04:00 UTC ежедневно).');
  1. Сервис ProjectPurgeService (псевдокод):
// projects:purge-deleted (Laravel scheduler / cron)
public function handle(): int {
    $enabled = SystemSetting::get('projects_purge_deleted_enabled') === 'true';
    if (!$enabled) return 0;

    $ttlDays = (int) SystemSetting::get('projects_purge_deleted_ttl_days');
    $cutoff  = now()->subDays($ttlDays);

    return Project::onlyTrashed()
        ->where('deleted_at', '<', $cutoff)
        ->each(function ($project) {
            // Каскадно: deals + project_suppliers через FK ON DELETE CASCADE
            $project->forceDelete();
            ActivityLog::write('project_purged', ['project_id' => $project->id]);
        })->count();
}

Биз-14: TTL = 6 месяцев (180 дней), cron disabled по умолчанию (включается клиентом / админом). Дефолт включается после Б-1 (юр. готовность).

2.18. 🔴 «Тревожные паттерны безопасности оригинала» — мы избегаем (новое из партий 14–15)

Партии 14–15 выявили 2 анти-паттерна безопасности в оригинале, которых мы должны строго избегать:

2.19.1. Пароль plaintext в HTML-форме (партия 14.3.4)

В оригинале: на странице /admin/user/account в блоке «Системная информация» поле «Текущий пароль» — это <input type="text"> с заполненным значением пароля пользователя в plaintext в HTML. Смена пароля — inline-вводом нового значения в это же поле и общим POST-запросом формы профиля.

Угрозы:

  • При просмотре страницы сессии пароль видим в DevTools → Elements.
  • При XSS-атаке на страницу профиля пароль угоняется без user interaction.
  • При случайном скриншоте/демо пароль попадает в кадр.

У нас должно быть:

  • Отдельная защищённая форма «Старый пароль / Новый пароль / Подтверждение нового».
  • Все 3 поля — <input type="password"> с возможностью toggle «показать»/«скрыть».
  • В HTML страницы пароль никогда не пре-заполнен.
  • Rate-limit на смену пароля (3 попытки/час).
  • Проверка сложности нового пароля (min 12, есть верхний+нижний+цифра+спецсимвол).

2.19.2. API-key и client_secret в <input type="text"> (партия 15.2.3)

В оригинале: в карточках интеграций (Скорозвон, Мои Звонки, Bitrix24, amoCRM, Unisender) поля api_key, client_secret, client_id, token — все имеют type="text", не type="password".

Угрозы:

  • При просмотре заполненной карточки администратором другие люди в комнате видят токены на экране.
  • В скриншотах для саппорта токены попадают в чат-логи.
  • В DevTools → Network значения видны в plain в response body.

У нас должно быть:

  • Все credentials-поля — <input type="password"> с button-toggle «👁 показать».
  • В API-response при чтении карточки — credentials заменяются на ***...**** (показываются только последние 4 символа).
  • Полное значение возвращается только при явном «отредактировать» — после повторной аутентификации админа.
  • В журнал действий пишем «секрет изменён» без значения.

Оба паттерна — обязательное усиление безопасности относительно паритета, фиксируем в §22 narrative.

2.19. 🟢 В оригинале нет 2FA, нет журнала входов, нет sessions — наше преимущество (партия 14)

Что зафиксировано в партии 14.3.2-14.3.4. На странице /admin/user/account оригинала отсутствуют разделы:

  • Журнал входов (/admin/user/auth-log отдаёт пустую страницу 217 байт).
  • Список активных сессий (/admin/user/sessions — заглушка «Технические работы»).
  • Управление устройствами (/admin/user/devices — пустая 217 байт).
  • 2FA (TOTP / SMS / Email-OTP / hardware key) — /admin/user/2fa пустая.
  • Recovery codes.

Что у нас уже есть в schema.sql v8.2 (Биз-9):

  • auth_log — таблица записей всех попыток входа (success/fail, IP, UA, geo).
  • user_sessions — таблица активных сессий с возможностью «Завершить сессию».
  • Биз-9 предусматривает TOTP (Google Authenticator) + recovery codes.

Маркетинговое позиционирование:

  • В §22 narrative — отдельный блок «Безопасность аккаунта» с разделами: Пароль, 2FA, История входов, Активные сессии, Уведомления о подозрительной активности.
  • В сравнительной таблице vs оригинал — этот блок целиком в нашу пользу.

SMS / Email-OTP — Post-MVP (требуют интеграции с SMS-шлюзом, удорожают MVP без явной необходимости).


3. Изменения в модели данных (DDL для schema.sql v8.2 и v8.3)

3.1. Сущность «Поставщик» (для §2.1)

-- =============================================================================
-- 3.1. Поставщики данных (suppliers)
-- =============================================================================
-- Контекст: реселлерская модель Ю-2 → мы покупаем лиды у crm.bp-gr.ru.
-- crm.bp-gr.ru внутри себя имеет 3 канала приёма (B1/B2/B3) — это
-- "суб-поставщики". Для нас на MVP — один уровень (suppliers), B1/B2/B3
-- хранятся как 3 отдельные строки. Это упрощает RLS, FK и счета.
-- Если в будущем появятся другие первичные поставщики (не crm.bp-gr.ru),
-- добавляется поле parent_supplier_id или вводится отдельная иерархия.
-- =============================================================================

CREATE TABLE suppliers (
    id              BIGSERIAL PRIMARY KEY,
    code            VARCHAR(50) NOT NULL UNIQUE,         -- 'b1', 'b2', 'b3' (расширяемо)
    name            VARCHAR(255) NOT NULL,               -- 'B1 — Сайты/Звонки', 'B2 — СМС с ключевым словом'
    description     TEXT,
    accepts_types   VARCHAR(50)[] NOT NULL,              -- ['websites','calls'] | ['sms']
    cost_rub        DECIMAL(10,2) NOT NULL,              -- закупочная цена ОДНОГО лида
    is_active       BOOLEAN DEFAULT TRUE,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ
);

CREATE INDEX idx_suppliers_active ON suppliers(is_active) WHERE is_active = TRUE;

-- Seed-данные (после миграции — INSERT):
-- INSERT INTO suppliers (code, name, accepts_types, cost_rub) VALUES
--   ('b1', 'B1 — Сайты и Звонки',          ARRAY['websites','calls'], 1.00),
--   ('b2', 'B2 — СМС с ключевым словом',   ARRAY['sms'],              1.00),
--   ('b3', 'B3 — СМС по наименованию',      ARRAY['sms'],              1.00);


-- Связь "проект ↔ поставщики" (один проект может принимать от нескольких,
-- по умолчанию все три checked в форме создания — паритет с оригиналом).
CREATE TABLE project_suppliers (
    project_id      BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    supplier_id     BIGINT NOT NULL REFERENCES suppliers(id),
    -- Per-supplier настройки (опционально, JSONB для гибкости):
    settings        JSONB DEFAULT '{}'::jsonb,           -- например: для B2 — {"sender_name":"...","keyword":"..."}
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (project_id, supplier_id)
);

CREATE INDEX idx_project_suppliers_supplier ON project_suppliers(supplier_id);

Миграция существующего поля supplier_lead_costs.supplier_code:

-- Удаляем default, добавляем supplier_id, бэкфилим, делаем NOT NULL.
ALTER TABLE supplier_lead_costs
    ADD COLUMN supplier_id BIGINT REFERENCES suppliers(id);

UPDATE supplier_lead_costs
   SET supplier_id = (SELECT id FROM suppliers WHERE code = 'b1')
 WHERE supplier_id IS NULL;

ALTER TABLE supplier_lead_costs
    ALTER COLUMN supplier_id SET NOT NULL,
    DROP COLUMN supplier_code;

CREATE INDEX idx_supplier_lead_costs_supplier ON supplier_lead_costs(supplier_id);

Важно для RLS. Таблицы suppliers, project_suppliersНЕ tenant-уровневые (это наши поставщики, общие для всех тенантов). RLS не применяется. Чтение разрешено всем crm_app_user, запись — только crm_admin_user.

system_settings.supplier_default_cost_rub — оставляем для обратной совместимости (используется как fallback при создании новой строки suppliers), но новый код должен читать cost_rub из конкретной строки suppliers.

3.2. Динамические лимиты проектов (для §2.2)

-- =============================================================================
-- 3.2. Расширение projects под динамические лимиты
-- =============================================================================

ALTER TABLE projects
    ADD COLUMN daily_limit_target          INT NOT NULL DEFAULT 10,
        -- что хочет клиент (default 10 = паритет с оригиналом)
    ADD COLUMN effective_daily_limit_today INT,
        -- что реально на сегодня после автокоррекции (NULL = не считалось)
    ADD COLUMN effective_limit_calculated_at TIMESTAMPTZ,
        -- когда последний раз пересчитывали
    ADD COLUMN region_mask                 INT NOT NULL DEFAULT 255,
        -- битмаска 8 федеральных округов РФ (паритет с оригиналом, секция 7 карточки)
        -- бит 1=Центральный, 2=Северо-Западный, ..., 128=Дальневосточный
        -- 255 = все 8 округов разрешены (default)
    ADD COLUMN region_mode                 VARCHAR(10) DEFAULT 'include'
        CHECK (region_mode IN ('include','exclude')),
        -- toggle "Включить/Исключить регионы" (паритет с оригиналом, секция 6)
    ADD COLUMN delivery_days_mask          INT NOT NULL DEFAULT 127
        -- битмаска 7 дней недели для приёма лидов (Пн=1, Вт=2, ..., Вс=64)
        -- 127 = все 7 дней (паритет с оригиналом, секция 11)
    ;

COMMENT ON COLUMN projects.effective_daily_limit_today IS
    'Реальный лимит на сегодня после автокоррекции по балансу. Пересчитывается '
    'cron-ом limits:recalc в 00:00 МСК и при каждом пополнении/списании баланса.';

-- Лог автокоррекций — отдельная таблица для аналитики и compliance.
CREATE TABLE project_limit_adjustments (
    id              BIGSERIAL PRIMARY KEY,
    tenant_id       BIGINT NOT NULL,
    project_id      BIGINT NOT NULL,
    target_limit    INT NOT NULL,
    effective_limit INT NOT NULL,
    reason          VARCHAR(50) NOT NULL
        CHECK (reason IN ('balance_low','manual_override','tariff_change','daily_recalc')),
    balance_at_calc DECIMAL(12,2),
    lead_cost_at_calc DECIMAL(10,2),
    calculated_at   TIMESTAMPTZ DEFAULT NOW(),
    -- FK на projects не ставим (RLS будет идти по tenant_id, project_id)
    INDEX idx_limit_adj_project (project_id, calculated_at DESC)
);

-- RLS-политика
ALTER TABLE project_limit_adjustments ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON project_limit_adjustments
    FOR ALL TO crm_app_user
    USING (tenant_id = current_setting('app.current_tenant_id')::BIGINT);

Сервис EffectiveLimitCalculator (псевдокод):

// Вызывается:
//   1. Cron-ом limits:recalc в 00:00 МСК для всех активных проектов.
//   2. При пополнении баланса (после commit balance_transactions).
//   3. При списании за лид (если effective_limit < target_limit).
//   4. При создании проекта.
//   5. При смене тарифа.

public function recalculate(Project $project): int {
    $tenant     = $project->tenant;
    $balance    = $tenant->balance_rub;
    $leadCost   = $tenant->effective_lead_cost();   // из тарифа клиента
    $maxByMoney = (int) floor($balance / $leadCost);
    $effective  = min($project->daily_limit_target, $maxByMoney);

    if ($effective !== $project->effective_daily_limit_today) {
        ProjectLimitAdjustment::create([...]);    // лог
        $project->effective_daily_limit_today = $effective;
        $project->effective_limit_calculated_at = now();
        $project->save();
        ActivityLog::write('auto_limit_adjustment', ...);
    }

    return $effective;
}

3.3. Уже запланированные изменения (повтор для полноты картины)

Из решения интервью OPEN-Д-1 (Прил. Д v8.2):

ALTER TABLE pd_subject_requests
    ADD COLUMN processing_restricted BOOLEAN NOT NULL DEFAULT FALSE;

COMMENT ON COLUMN pd_subject_requests.processing_restricted IS
    'Реализация ст.21 152-ФЗ. При TRUE — все операции с ПДн субъекта '
    'блокируются на уровне сервиса (ProcessingRestrictedException) и БД (RLS).';

Из решения интервью OPEN-Д-5 / OPEN-И-1 (Прил. Д v8.2 + Прил. И v8.2):

CREATE TABLE incidents_log (
    id                            BIGSERIAL PRIMARY KEY,
    type                          VARCHAR(50) NOT NULL,
    severity                      VARCHAR(20) NOT NULL,
    started_at                    TIMESTAMPTZ NOT NULL,
    detected_at                   TIMESTAMPTZ NOT NULL,
    resolved_at                   TIMESTAMPTZ,
    summary                       TEXT NOT NULL,
    root_cause                    TEXT,
    postmortem_url                VARCHAR(500),
    related_pd_subject_request_ids BIGINT[],
    created_by_admin_id           BIGINT REFERENCES saas_admin_users(id),
    created_at                    TIMESTAMPTZ DEFAULT NOW(),
    CHECK (severity IN ('low','medium','high','critical')),
    CHECK (resolved_at IS NULL OR resolved_at >= started_at)
);

CREATE INDEX idx_incidents_started ON incidents_log(started_at DESC);
CREATE INDEX idx_incidents_severity_unresolved ON incidents_log(severity)
    WHERE resolved_at IS NULL;

Итого: все 4 группы изменений выше попадут в schema.sql v8.2.

3.4. Сводка добавляемых таблиц и полей в v8.2 (применено)

Что Где Размер изменения
Новая таблица suppliers §3.1 1 таблица + 1 индекс + seed
Новая таблица project_suppliers §3.1 1 таблица + 1 индекс
Миграция supplier_lead_costs.supplier_codesupplier_id §3.1 ALTER + UPDATE + DROP
6 новых полей в projects §3.2 ALTER TABLE
Новая таблица project_limit_adjustments §3.2 1 таблица + RLS
Новое поле pd_subject_requests.processing_restricted §3.3 ALTER TABLE
Новая таблица incidents_log §3.3 1 таблица + 2 индекса
Всего в v8.2 +4 таблицы, +7 полей

3.5. Дополнения в schema.sql v8.3 (по итогам партий 12–15)

3.5.1. Перепись таблицы reminders (для §9.1, Биз-10 переоткрыт)

Контекст партии 12.2.5: в оригинале model.histories[] содержит записи type='reminder' — то есть множество напоминаний на сделку поддерживается на уровне схемы (хотя UI обычно показывает одно). У нас в schema v8.2 — поля deals.reminder_text + deals.reminder_at (одиночное) + уже была таблица reminders с полями user_id, is_done. Нужно: убрать дублирование (поля из deals), переименовать user_id → created_by (по партии 12.2.4 это histories[].from), добавить assignee_id и completed_at.

-- =============================================================================
-- 3.5.1. Reminders v8.3 — переписана для паритета с histories[].type='reminder'
-- =============================================================================

CREATE TABLE reminders (
    id              BIGSERIAL PRIMARY KEY,
    tenant_id       BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    deal_id         BIGINT NOT NULL,                     -- БЕЗ FK (deals партиционирована)
    text            VARCHAR(255),                        -- = note в оригинале, лимит 255
    remind_at       TIMESTAMPTZ NOT NULL,
    created_by      BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                                                         -- = histories[].from
    assignee_id     BIGINT REFERENCES users(id),         -- зарезервировано (= to, NULL в оригинале)
    completed_at    TIMESTAMPTZ,                         -- наше расширение
    is_sent         BOOLEAN DEFAULT FALSE,               -- для cron нотификаций
    sent_at         TIMESTAMPTZ,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ
);

CREATE INDEX idx_reminders_due
    ON reminders(remind_at) WHERE is_sent = FALSE AND completed_at IS NULL;
CREATE INDEX idx_reminders_deal
    ON reminders(deal_id);
CREATE INDEX idx_reminders_tenant_user_active
    ON reminders(tenant_id, created_by, remind_at) WHERE completed_at IS NULL;
CREATE INDEX idx_reminders_tenant_active
    ON reminders(tenant_id, remind_at) WHERE completed_at IS NULL;

-- RLS уже была в v8.2, остаётся валидной (политика на tenant_id):
-- ALTER TABLE reminders ENABLE ROW LEVEL SECURITY;
-- CREATE POLICY tenant_isolation ON reminders
--     USING (tenant_id = current_setting('app.current_tenant_id')::bigint);

Удаление полей из deals:

-- В консолидированной schema.sql v8.3 — поля просто отсутствуют (ALTER не нужен).
-- Для миграции существующего dev-окружения с v8.2 → v8.3:
INSERT INTO reminders (tenant_id, deal_id, text, remind_at, created_by, created_at)
  SELECT 
      d.tenant_id, d.id, d.reminder_text, d.reminder_at,
      COALESCE(d.manager_id, (SELECT id FROM users WHERE tenant_id = d.tenant_id LIMIT 1)),
      d.received_at
    FROM deals d
   WHERE d.reminder_at IS NOT NULL;

ALTER TABLE deals DROP COLUMN reminder_text;
ALTER TABLE deals DROP COLUMN reminder_at;
DROP INDEX IF EXISTS idx_deals_reminder;

3.5.2. Расширение suppliers capabilities (для §9.2)

Контекст партии 13.3.5: UI оригинала явно различает возможности B1/B2/B3 — оранжевый notice в карточке проекта: «Указать в связке 'Наименование отправителя' и 'Ключевое слово' можно только по поставщику B2. Поставщик B3 работает только по наименованию отправителя».

ALTER TABLE suppliers
    ADD COLUMN channel               VARCHAR(20) NOT NULL DEFAULT 'sites'
        CHECK (channel IN ('sites','calls','sms')),
    ADD COLUMN supports_sender_name  BOOLEAN NOT NULL DEFAULT FALSE,
    ADD COLUMN supports_keyword      BOOLEAN NOT NULL DEFAULT FALSE,
    ADD COLUMN supports_csv_upload   BOOLEAN NOT NULL DEFAULT TRUE,
    ADD COLUMN supports_domains_list BOOLEAN NOT NULL DEFAULT TRUE;

UPDATE suppliers SET 
    channel = 'sites', supports_sender_name = FALSE, supports_keyword = FALSE
  WHERE code = 'b1';

UPDATE suppliers SET 
    channel = 'sms', supports_sender_name = TRUE, supports_keyword = TRUE,
    supports_csv_upload = FALSE, supports_domains_list = FALSE
  WHERE code = 'b2';

UPDATE suppliers SET 
    channel = 'sms', supports_sender_name = TRUE, supports_keyword = FALSE,
    supports_csv_upload = FALSE, supports_domains_list = FALSE
  WHERE code = 'b3';

В UI: при выборе поставщика(ов) для проекта frontend проверяет capabilities и показывает только релевантные поля. Если выбраны несколько — пересечение (B2+B3 → можно sender_name, нельзя keyword).

3.5.3. Поле tenants.desired_daily_numbers (для §9.2, Биз-16)

Контекст партии 13.2.2: аккаунтная настройка «Желаемое кол-во номеров в день» — целевой объём для бэк-офиса. Не лимит, не биллинг — сигнал саппорту.

ALTER TABLE tenants
    ADD COLUMN desired_daily_numbers INT
        CHECK (desired_daily_numbers IS NULL OR desired_daily_numbers > 0);

COMMENT ON COLUMN tenants.desired_daily_numbers IS
    'Целевое количество лидов в день, которое клиент хочет получать. '
    'Не лимит, не биллинг — сигнал для саппорта (Биз-16). NULL = не указано.';

3.5.4. Soft-delete проектов с TTL (для §9.2, Биз-14)

Контекст партии 13.2.3: оригинал хранит soft-deleted проекты ≥3 месяца, точное TTL не определено. Делаем cron projects:purge-deleted с настраиваемым TTL = 6 месяцев + cron disabled по умолчанию.

INSERT INTO system_settings (key, value, type, description) VALUES
  ('projects_purge_deleted_enabled', 'false', 'bool',
   'Включить ли cron физического удаления soft-deleted проектов'),
  ('projects_purge_deleted_ttl_days', '180', 'int',
   'TTL для физического удаления (по умолчанию 6 мес)'),
  ('projects_purge_deleted_cron', '0 4 * * *', 'string',
   'Расписание cron projects:purge-deleted (04:00 МСК ежедневно)');

3.5.5. Сводка изменений v8.2 → v8.3

Что Где Размер изменения
Перепись таблицы reminders (поля + индексы) §3.5.1 -2 поля + 3 поля + новые индексы
Удаление deals.reminder_text, reminder_at, индекс §3.5.1 -2 поля, -1 индекс
Расширение suppliers (5 полей capabilities) §3.5.2 +5 полей
Новое поле tenants.desired_daily_numbers §3.5.3 +1 поле
3 новых записи в system_settings §3.5.4 +3 settings
Всего в v8.3 +4 поля (нетто), +1 индекс (нетто)

Итоговые параметры schema.sql v8.3:

Метрика v8.2 v8.3
Логических таблиц 51 51 (без изменений: reminders уже была)
Полей в deals (текущее) -2 (убраны reminder_text, reminder_at)
Полей в suppliers 8 13 (+5 capabilities)
Полей в tenants (текущее) +1 (desired_daily_numbers)
Поля reminders user_id, is_done created_by, assignee_id, completed_at
Индексов 80 81 (+2 в reminders, -1 idx_deals_reminder)
RLS-политик 31 31 (без изменений)
Записей в system_settings 22 25 (+3)

4. Изменения в narrative (для v8.4)

4.1. §1 (Обзор системы) — раздел про конкурентные преимущества

Добавить блок «Что мы умеем, чего нет в оригинале»:

  1. Outbound webhook к тенанту (§19) — оригинал только принимает, мы и принимаем, и отдаём.
  2. Полный аудит мутаций сделки (§14) — оригинал не логирует смену ответственного / проекта / телефона / суммы; мы логируем всё.
  3. Kanban-доска с drag-and-drop (§11) — у оригинала только табличный вид.
  4. Real-time push-уведомления (§17) — у оригинала обновления только перезагрузкой.
  5. Дашборд с настраиваемыми виджетами (§12) — у оригинала фиксированный дашборд с 5 KPI.
  6. API для CRM-интеграций (amoCRM, Bitrix24) (OPEN-И-2, спринт 14–15) — у оригинала нет.

4.2. §5 (Источник данных — Webhook от crm.bp-gr.ru) — формат ingress

Уточнить формат входящего payload по аудиту партии 3.1:

GET/POST https://crm.bp-gr.ru/api/...
Параметры:
  vid       — токен пользователя (наш кабинет в crm.bp-gr.ru)
  project   — id проекта в crm.bp-gr.ru (мапинг на наш project_id через тег)
  tag       — короткий тег кампании (B1_..., B2_..., B3_..., наш ключ для project_suppliers)
  phone     — основной телефон лида
  time      — отметка времени
  phones[]  — массив доп. телефонов

Добавить раздел «Извлечение поставщика из тега»: при приёме webhook парсим префикс тега (B1_carmoney → supplier_code='b1'), создаём запись supplier_lead_costs с правильным supplier_id.

4.3. §7 (Сущности и схема БД) — описание новых сущностей

Добавить разделы:

  • §7.6.1 — suppliers (B1/B2/B3, расширяемо)
  • §7.6.2 — project_suppliers (m2m с per-supplier настройками)
  • §7.6.3 — project_limit_adjustments (лог автокоррекций)
  • §7.7 — расширение projects (5 новых полей: daily_limit_target, effective_daily_limit_today, effective_limit_calculated_at, region_mask, region_mode, delivery_days_mask)

4.4. §8 (Воронка продаж и статусы) — явная фиксация free state machine

Добавить пункт в начало раздела:

Переходы статусов сделок — свободные, без машины состояний. Из любого статуса (включая терминальные closed и final-non-dial) разрешён переход в любой другой. Это паритет с оригиналом crm.bp-gr.ru (партия 11.3 аудита). Цель — менеджер может «откатить» статус при возврате клиента, что является нормальным сценарием в лидген-CRM.

4.5. §9 (Мои Проекты) — карточка проекта по аудиту

Описать карточку проекта в нашем варианте по структуре из партии 10.3, с нашими расширениями:

  • Активный — toggle (паритет).
  • Тег — text (паритет).
  • Поставщики (B1/B2/B3) — мульти-чекбокс (паритет, у нас через project_suppliers).
  • Название проекта — text (паритет).
  • Тип данных — select [websites/calls/sms] (наше расширение, в оригинале вычисляется по поставщикам).
  • Регионы — древовидный мульти-select 8 округов (паритет).
  • Режим регионов — toggle Include/Exclude (паритет).
  • Дни приёма — 7 чекбоксов битмаски (паритет).
  • Целевой дневной лимит — number, default 10 (паритет).
  • Реальный дневной лимит сегодня — readonly с подписью «авторегулировка по балансу» (наше улучшение для прозрачности).
  • Список доменов / Звонки / СМС-поля — textarea + загрузка CSV/XLS до 1 МБ (паритет).
  • Outbound webhook — наша фича (URL + secret + retry settings) — наше расширение.

4.6. §12 (Дашборд и аналитика) — экран «Конверсия проектов»

Явно добавить экран «Конверсия проектов» как первоклассный отчёт:

  • Таблица проект × статус, по строке на каждый активный проект.
  • Цветные ячейки по плотности значений (heatmap).
  • Расчётные колонки конверсий: new → paid (%), new → closed (%).
  • Фильтр по периоду + по тегу + по поставщику.
  • Экспорт в XLSX.

4.7. §14 (Журнал действий) — подсветить полный аудит

Добавить отдельный блок:

Что мы логируем (расширенный список):

  • Смена статуса (паритет с оригиналом).
  • Создание / изменение комментария (паритет).
  • Исходящий звонок (паритет).
  • Смена ответственного менеджера (наше улучшение — оригинал не логирует).
  • Смена проекта сделки (наше улучшение).
  • Изменение телефона (наше улучшение).
  • Изменение суммы (наше улучшение).
  • Изменение напоминания (наше улучшение).

Это закрывает аудит-пробел оригинала, выявленный в партии 11.4 аудита.

4.8. §17 (Напоминания и push-уведомления) — «тихие часы»

Добавить раздел про окно «Не беспокоить» (паритет с «Тихими часами» оригинала):

// users.notification_preferences — расширение
{
  "channels": { /* матрица 8×3, как в CTO-4 */ },
  "sound_enabled": true,
  "quiet_hours": {
    "enabled": true,
    "from": "22:00",
    "to": "08:00",
    "timezone": "Europe/Moscow"
  }
}

Логика: при отправке push-уведомления и SMS — если текущее время в окне quiet_hours, откладываем до конца окна (для не-критичных типов).

4.9. §22 (Безопасность) — раздел про prompt injection в DOM

Добавить новый раздел «22.X — Защита от prompt injection в DOM нашей платформы»:

По итогам аудита оригинала crm.bp-gr.ru обнаружена prompt injection-атака в DOM (раздел 6 этого приложения). Чтобы наша платформа не стала аналогичной целью для AI-агентов клиентов:

  1. CSP (Content Security Policy): запретить inline-скрипты, разрешить только наши домены и явный whitelist (JivoSite, Sentry, Yandex Cloud SDK).
  2. Запрет HTML-инъекций в пользовательский ввод: все поля (комментарии, заметки, имена) санитизируются через DOMPurify с минимальным whitelist тегов.
  3. Запрет маркеров для агентов: в нашем DOM не должно быть элементов с ID вида claude-*, gpt-*, agent-*.
  4. Аудит сторонних виджетов: перед подключением (JivoSite и др.) — проверка скрипта на наличие подобных маркеров. При обнаружении — отказ от интеграции или прокcирование через наш CDN.

4.10. §26 (Дизайн-система / Frontend) — стек

В разделе «Что мы НЕ делаем» добавить:

Мы НЕ повторяем стек оригинала (Vue 2 + Vuetify 1.5 + Element UI + jQuery + Bootstrap + Yii2 одновременно). У нас один frontend-фреймворк (Vue 3) + одна UI-библиотека (Vuetify 3) + чистый Laravel API на бэкенде. Это решение фундаментальное и не подлежит пересмотру, так как стек оригинала является результатом многолетней органической эволюции без рефакторинга и не должен повторяться.


5. Новые продуктовые вопросы к заказчику

По итогам аудита возникли 7 новых продуктовых вопросов, требующих решения заказчика. Они не блокируют разработку (есть разумные дефолты), но желательно закрыть до старта соответствующих спринтов.

Биз-10/11/12/13 — добавлены в v1.0 по итогам партий 1–11. Биз-14/15/16 — добавлены в v1.1 по итогам партий 12–15.

Биз-10 — Модель «задач» сделки (УТОЧНЕНО в v1.1)

Контекст: в оригинале массив напоминаний на сделку через model.histories[].type='reminder' (партия 12 уточнила гипотезу из партии 11). Реальная модель — список записей {id, note, time, time_dt, from, to}, где from = логин автора, to = NULL. Эмпирически в UI обычно одно напоминание на сделку, но архитектура поддерживает множество.

Поля минимального паритета (форма из партии 12.2.3): text VARCHAR(255), remind_at, created_by (=from), assignee_id NULL (=to, не используется в UI), created_at. Без приоритетов / каналов / recurrence — этих полей в форме оригинала нет.

Вопрос: оставляем минимальный паритет (массив напоминаний без приоритетов/каналов) или делаем улучшение (массив задач с типами / приоритетами / исполнителями)?

Рекомендация Claude: минимальный паритет на MVP, расширения — Post-MVP. Причина: лидген-CRM работают в режиме «быстрая обработка много лидов», классический task management избыточен. Усложнение UI без явного спроса — отложить.

DDL: см. §3.5.3 — отдельная таблица reminders, удаление полей deals.reminder_text/reminder_at.

Дефолт при отсутствии решения: минимальный паритет (отдельная таблица reminders с полями выше + completed_at как наше расширение для аналитики).

Биз-11 — Мульти-кошельковый биллинг

Контекст: в оригинале 4 раздельных счётчика в шапке (ГЦК / Диалоги КЦ / Минуты / Автозвонок). У нас один кошелёк в рублях с конвертацией в лиды по тарифу.

Вопрос: делаем нашу упрощённую модель (1 кошелёк) или повторяем телеком-модель оригинала (4 кошелька)?

Рекомендация Claude: наша упрощённая модель. Причины:

  1. 4 кошелька — наследие телекома (минуты + диалоги + автозвонок), это операции, которых у нас нет (мы не звоним и не диалогируем).
  2. У нас одна операция — приём лида, и одна валюта на покупку — рубль. Усложнение биллинга без необходимости.
  3. Стандарт SaaS-индустрии — единый баланс с конвертацией, бухгалтерия проще.

Дефолт при отсутствии решения: наша упрощённая модель (1 кошелёк).

Биз-12 — Телефонная интеграция

Контекст: в оригинале телефония через внешний CDN «Скорозвон». В нашей модели телефония отсутствует.

Вопрос: нужна ли телефонная интеграция на MVP? Если да — со «Скорозвоном» (используется оригиналом, понятная архитектура), MANGO Office (популярнее, лучше API), Telphin (бюджетный) или другим провайдером?

Рекомендация Claude: на MVP — НЕ делаем, добавляем как Post-MVP при первом запросе клиента. Причины:

  1. Это инфраструктурная интеграция (~1.5–2 спринта на одного провайдера).
  2. Не все клиенты колл-центры — для CRM-клиентов (которым важно accept лида, отдать менеджеру, потом зайти в свою телефонию) телефония внутри CRM избыточна.
  3. На MVP лучше иметь хороший outbound webhook (уже планируется в OPEN-И-2), через который любая внешняя телефония может подписаться на новые лиды.

Дефолт при отсутствии решения: не делаем на MVP.

Контекст: в оригинале админ генерирует одноразовую ссылку для менеджера (24ч), менеджер заходит без пароля. Удобно для подрядных колл-центров с большой текучкой.

Вопрос: делаем magic-link 24ч для менеджеров на MVP?

Рекомендация Claude: да, делаем на MVP — это маленькая фича (~0.3 спринта), но даёт явное конкурентное преимущество для целевой аудитории (колл-центры с подрядными операторами). Реализация: новая таблица manager_magic_links(token_hash, manager_id, expires_at, used_at, created_by_admin_id), кнопка «Создать ссылку входа» в интерфейсе управления менеджерами, страница логина по ссылке.

Дефолт при отсутствии решения: делаем на MVP.


Биз-14 — TTL для soft-deleted проектов (новый, добавлен в v1.1)

Контекст: в оригинале (партия 13.2.3) экран «Удалённые проекты ГЦК» (/admin/visit/rt?type=deleted) показывает проекты, удалённые минимум 96 дней назад (запись «удалено 2026-01-29» открыта в аудите 2026-05-05). У нас в schema v8.2 поле projects.deleted_at есть, но retention policy не определена.

Вопрос: через какой срок soft-deleted проекты:

  • (а) удаляются физически (DROP) — навсегда, без восстановления;
  • (б) удаляются с обнулением ПДн (anonymized = true), но сохраняются для compliance-журнала;
  • (в) хранятся бессрочно, без очистки.

Рекомендация Claude: 180 дней (6 месяцев) + cron projects:purge-deleted, выключен по умолчанию до Б-1 (реквизиты ООО). Логика: после 6 месяцев проект и все его лиды (с ПДн) полностью удаляются — это адекватный срок для разрешения возможных споров с клиентом и аудитов от РКН. Cron включается админом через system_settings.projects_purge_deleted_enabled = true после получения юридического согласования. До включения cron — все «удалённые» проекты живут в БД бессрочно, как сейчас в оригинале.

Дефолт при отсутствии решения: 180 дней + cron disabled. См. §3.5.4 для DDL и Прил. И для runbook.


Биз-15 — Wizard «Рекомендации источников» (OSINT) (новый, добавлен в v1.1)

Контекст: в оригинале (партия 13.1) есть полноценная фича /admin/gck/dop-sources — wizard на 3 шага: «Поиск (по доменам/ключевикам конкурентов) → Результат (рекомендованные домены/телефоны) → Чёрный список». Это OSINT-инструмент для самостоятельного поиска новых источников трафика клиентом — отдельная сущность, не связана с управлением поставщиками B1/B2/B3.

Вопрос: делаем такой wizard в Лидерре на MVP?

Рекомендация Claude: не делаем на MVP, в Post-MVP по запросу. Аргументы:

  • Бизнес-ценность спорна: для арбитражной модели «получи лиды от B1/B2/B3» эта фича — «помощник по конкурентам», не основной flow.
  • Реализация требует внешнего OSINT API (поиск конкурирующих доменов и связанных телефонов), затраты ~3 спринта.
  • Если делать — отдельные таблицы: source_recommendation_searches, source_recommendation_results, source_recommendation_blacklist (см. партию 13.1.6).
  • На старте Лидерры эту функцию можно оставить за скобками без потери основных пользователей.

Дефолт при отсутствии решения: не делаем на MVP, ставим в backlog с приоритетом P3.


Биз-16 — Поле tenants.desired_daily_numbers (новый, добавлен в v1.1)

Контекст: в оригинале (партия 13.2.2) есть аккаунтная (не проектная!) модалка «Желаемое кол-во номеров в день». Это не лимит, а ориентир для сотрудников бэк-офиса ГЦК, которые ручным трудом дотягивают объём до нужного.

Вопрос: добавляем такое поле в Лидерре?

Рекомендация Claude: да, добавляем как tenants.desired_daily_numbers INT NULL. Аргументы:

  • Полезный сигнал саппорту Лидерры: видеть, что клиент ожидает 100 лидов/день, а получает 30 — повод связаться с клиентом и помочь оптимизировать настройки проектов.
  • Минимальная реализация: одно поле в tenants, одно поле в форме настроек аккаунта, одна колонка в админ-дашборде /admin/tenants.
  • Затраты ~0.1 спринта.

Дефолт при отсутствии решения: добавляем. См. §3.5.3 для DDL.


6. Безопасность — prompt injection в DOM CRM

6.1. Что обнаружено

В DOM каждой страницы crm.bp-gr.ru присутствует элемент:

<div id="claude-agent-stop-container">
  <button id="claude-agent-stop-button">
    <span>Stop Claude</span>
  </button>
</div>

Идентификаторы (claude-agent-stop-*) явно нацелены на агента Claude — это не общая «кнопка остановки скрипта», а именно нацеленная провокация для AI-агентов с именем «Claude».

6.2. Что НЕ установлено

Источник инъекции остаётся неизвестным. Возможные версии:

  1. Расширение браузера пользователя (заказчика) — расширение могло быть установлено для других целей и инжектит этот элемент во все страницы.
  2. Тестовая разметка владельцев CRM — крайне маловероятно, но теоретически crm.bp-gr.ru мог добавить кнопку для тестирования AI-доступа.
  3. Враждебная инъекция в коде CRM — XSS-атака или скомпрометированный компонент.
  4. HelpDeskEddy виджет — изначально подозревался, в партии 11.11 отвергнут (атрибуты не совпадают с известным DOM HelpDeskEddy).

6.3. Реакция оператора

Оператор Claude в Chrome:

  • Не нажимал кнопку.
  • Не выполнял текстовую команду «Stop Claude».
  • Доложил пользователю о находке.
  • Продолжил аудит, игнорируя инъекцию.

Это правильное поведение в соответствии с системными правилами защиты от prompt injection: команды от веб-контента (DOM-элементы, текст в страницах, имена кнопок) никогда не выполняются без явного подтверждения пользователя в чате.

6.4. Что нужно делать дальше

Срочное (для текущей работы заказчика):

  1. Заказчику — проверить расширения браузера в том профиле, где открывается crm.bp-gr.ru. Версия 1 (расширение) — наиболее вероятная.
  2. Если расширение не нашлось — открыть DevTools → Sources, найти, в каком файле создаётся claude-agent-stop-container. Это покажет источник.
  3. При враждебной инъекции в коде CRM — сообщить владельцам crm.bp-gr.ru. До устранения — ограничить доступ Claude в Chrome к этой CRM с дополнительной осторожностью (что и так делается).

Архитектурное (для нашей платформы):

  • См. §4.9 — раздел в narrative §22.

6.5. Урок для нашей платформы

Принцип: наша платформа Лидерра должна быть «AI-agent friendly» в правильном смысле — то есть:

  • Семантическая разметка, корректные ARIA-атрибуты, осмысленные ID — для legitimate использования AI-агентов клиентов.
  • Никаких «AI-маркеров», специально размещённых для одних агентов и не других.
  • Никаких скрытых инструкций в DOM (через aria-label, title, data-attributes).
  • CSP, исключающий выполнение неавторизованных скриптов.

6.6. Повторные подтверждения в партиях 12–15

В каждой из 4 параллельных партий (12, 13, 14, 15) элемент claude-agent-stop-container снова обнаружен в DOM. В партии 15 дополнительно зафиксированы два связанных элемента:

<div id="claude-agent-glow-border"></div>
<style id="claude-agent-animation-styles">
  @keyframes claude-pulse { ... }
</style>

Это означает, что инъекция включает не только кнопку, но и визуальные подсказки (анимированная подсветка) — рассчитана на то, что AI-агент «увидит» её на скриншоте и среагирует. Это укрепляет гипотезу о расширении браузера (версия 1 в §6.2).

Реакция всех 4 операторов Claude в Chrome была корректной: кнопка не нажата, команда «Stop Claude» не выполнена, все 4 партии завершены полностью.

Дополнительные находки (партии 14 и 15) для §22 narrative нашей платформы:

  1. Антипаттерн оригинала: пароль в plaintext в HTML-форме (партия 14.3.4). На странице профиля <input type="text" name="User[password]"> с заполненным значением. У нас — отдельная защищённая форма «старый/новый/подтверждение» с type="password" + rate-limit.

  2. Антипаттерн оригинала: API credentials в <input type="text"> (партия 15.2). Все api_key, client_secret, token хранятся как обычный текстовый input. У нас — type="password" + кнопка «👁 Показать», маскирование при сохранении (отображаются только последние 4 символа), возможность «перевыпустить» в один клик.

  3. Принцип «ZTA для credentials»: все credentials шифруются на уровне БД (column-level encryption через pgcrypto), ключ — в Yandex Lockbox.


7. Итоговая сводка изменений по архиву

7.1. Что меняется в schema.sql (v8.1 → v8.2 → v8.3)

v8.1 → v8.2 (применено 04.05.2026):

# Изменение Откуда
1 Новая таблица suppliers + seed B1/B2/B3 §3.1
2 Новая таблица project_suppliers §3.1
3 Миграция supplier_lead_costs.supplier_codesupplier_id §3.1
4 6 новых полей в projects (лимиты, регионы, дни) §3.2
5 Новая таблица project_limit_adjustments §3.2
6 Новое поле pd_subject_requests.processing_restricted §3.3, OPEN-Д-1
7 Новая таблица incidents_log §3.3, OPEN-И-1

v8.2 → v8.3 (применено 05.05.2026, по итогам партий 12–15):

# Изменение Откуда
8 Перепись reminders (created_by, assignee_id, completed_at) + 4 индекса §3.5.1 (партия 12.2)
9 Удаление deals.reminder_text, deals.reminder_at, idx_deals_reminder §3.5.1
10 Расширение suppliers 5 полями capabilities §3.5.2 (партия 13.3.5)
11 Новое поле tenants.desired_daily_numbers §3.5.3 (партия 13.2.2, Биз-16)
12 3 новых записи в system_settings для cron purge-deleted §3.5.4 (Биз-14)

7.2. Что меняется в narrative (v8.3 → v8.4)

10 разделов: §1 (конкурентные преимущества), §5 (формат ingress), §7 (новые сущности), §8 (free state machine), §9 (карточка проекта), §12 (Конверсия проектов), §14 (полный аудит), §17 (тихие часы), §22 (защита от prompt injection), §26 (стек).

7.3. Что меняется в Открытых вопросах (v1.4 → v1.5 → v1.6)

  • v1.4 → v1.5 (04.05.2026): добавлены 4 вопроса (Биз-10, 11, 12, 13) — раздел 11 «Новые вопросы из аудита оригинала».
  • v1.5 → v1.6 (05.05.2026): добавлены 3 вопроса (Биз-14, 15, 16) + переоткрыт Биз-10 — раздел 12 «Аудит партий 12–15». Все — с дефолтами, не блокеры.

Итого по аудиту 04–05.05.2026: 7 новых продуктовых вопросов, 1 переоткрытый (Биз-10).

7.4. Что появляется в архиве

  • Прил. М (этот файл) — постоянное приложение архива, обновляется при следующих аудитах. Текущая версия — v1.1 (партии 1–15).
  • crm-bp-gr-audit-2026-05-04.md — исходный материал партий 1–11 (заход 1).
  • audit-batch-12-2026-05-05.md — партия 12 (Конверсия + Напоминания + Досье).
  • audit-batch-13-2026-05-05.md — партия 13 (Рекомендации + Настройки + Просмотреть + Звонки).
  • audit-batch-14-2026-05-05.md — партия 14 (Редкие статусы + KB + Безопасность).
  • audit-batch-15-2026-05-05.md — партия 15 (карточки «API ▾», окончательный вердикт по webhook'ам).

7.5. Что меняется в README архива

  • Состав архива: добавлены 4 новых аудит-файла (партии 12–15).
  • В разделе «Что нового в v8.3++» — отдельный пункт про аудит партий 12–15 и связанные изменения (новая модель reminders, расширение suppliers capabilities, новое поле desired_daily_numbers, cron purge-deleted).

8. Что мне (Claude) нужно сделать после Прил. М

В порядке приоритета:

  1. Открытые_вопросы_v8_3.md → v1.5 — раздел 11 с Биз-10/11/12/13 (сделано 04.05).
  2. README_АРХИВ_v8_3.md → v8.3+ (сделано 04.05).
  3. schema.sql v8.2 — патч из §3.1–§3.4 (сделано 04.05).
  4. Открытые_вопросы → v1.6 — раздел 12 с Биз-14/15/16 (сделано 05.05).
  5. schema.sql v8.3 — патчи из §3.5 (сделано 05.05).
  6. Прил. Л — HTML/CSS/JS прототипы 8 экранов. Отдельная сессия.
  7. v8.4 narrative — раскрытие 30 решений интервью + интеграция выводов аудита 1–15 партий (§4 + §9.6 этого документа). Несколько сессий.

9. Результаты партий 12–15 (детально)

Параллельный аудит партий 12, 13, 14, 15 проведён 05.05.2026 через Claude в Chrome в 4 независимых окнах. Все 4 партии — read-only, без модификаций. Партия 15 проведена в режиме «полное раскрытие карточек интеграций со строгой маскировкой токенов через 6 регулярных выражений self-review»; токены и секреты не попали в отчёт.

9.1. Партия 12 — Конверсия проектов + Напоминания + Досье

9.1.1. Отчёт «Конверсия проектов» (/admin/visit/rt-stat) — структурно

Большая серверно-рендеримая таблица 17 колонок, 1 строка = 1 проект (всего 443 строки = всему реестру). Структура: id / Тег / Проект / Статус / Источник сбора / Кол-во обработанных (счётчик-знаменатель) / 11 колонок по статусам сделок (счётчик + %, формат N (XX.XX%)).

Цветовая схема — monotone per column (не heatmap, не светофор): у каждого статуса свой пастельный цвет фона, не зависящий от значения. CSS-классы переиспользуются (td_pereveden стоит у «Закрыто», «На замену», «Конечный недозвон» — легаси).

Формула: count_status_X / count_processed × 100. Знаменатель — единый по строке.

Что есть: total-row, фильтры по периоду / типу / поиску, чекбоксы видимости 17 колонок (хранятся в localStorage.rtstatopts1), сортировка по любой колонке.

Чего нет: drill-down в ячейку, экспорт XLSX/CSV.

Решение для Лидерры: реализовать как первоклассный отчёт §12 narrative. На бэке — один read-эндпоинт GET /api/projects/conversion?from=&to=&type=&search=. На фронте — Vue-таблица. Наше расширение сверх паритета: экспорт XLSX (через report_jobs) + drill-down из ячейки в отфильтрованный список сделок.

9.1.2. Напоминания — модель данных (Биз-10 переоткрыт)

Главное расхождение с гипотезой партии 11: в model.histories[] есть записи type='reminder' — то есть множество напоминаний на сделку поддерживается на уровне схемы. UI обычно показывает одно (это UX-конвенция), но архитектурно поддержано множественность.

Поля напоминания в оригинале (минимальный паритет):

Поле Источник в оригинале Назначение
text addReminder.note, лимит 255 Текст напоминания (необязательный)
remind_at date + hour + minute Когда напомнить
created_by histories[i].from (логин менеджера) Кто создал
created_at приходит с сервера Когда создано

Что отсутствует в оригинале: ответственный (to: null), приоритет, канал, повторение, привязка к статусу.

Бесплатный «дашборд задач»: дропдаун «Задачи» в шапке списка содержит 4 пункта с URL-параметрами ?reminders=today|last|future|none.

Решение для Лидерры: перепись таблицы reminders (см. §3.5.1). Удалить deals.reminder_text и deals.reminder_at. На MVP — поля паритета + completed_at (наше расширение). Расширения над паритетом — Post-MVP по запросу.

9.1.3. Модалка «Досье» — две разные сущности

В DOM сделки оказалось две модалки с одинаковым заголовком «Досье»:

Модалка №1 — baseInfo (простая v-dialog): триггер — карандаш рядом с телефоном гостя; условие — baseInfo.info.length > 0; назначение — «дубли по телефону».

Модалка №2 — visitdop (el-dialog с табами): триггер — кнопка «Просмотр досье» в .domen-right; содержимое — 2 таба (Досье с полями proceeds/profit/balance/arbitration + Pr-cy SEO-данные); эндпоинт GET /admin/visit/visit-dop-load?id={model.id}; назначение — внешние данные о домене сделки.

Решение для Лидерры:

  • Модалка №1 — Post-MVP (естественное расширение Биз-1).
  • Модалка №2 — НЕ делаем на MVP (требует интеграции с Pr-cy / Контур, для арбитражной CRM избыточно).

9.2. Партия 13 — Рекомендации + Настройки + Просмотреть + Звонки

9.2.1. Wizard «Рекомендации источников» (/admin/gck/dop-sources)

OSINT-инструмент по поиску внешних доменов и телефонов на основе ключевых запросов клиента. НЕ управление поставщиками B1/B2/B3 (гипотеза партии 10 опровергнута). 3-шаговый wizard, аккаунтный скоуп.

Решение для Лидерры: Биз-15 — на MVP не делаем, Post-MVP по запросу.

9.2.2. Аккордеон «Настройки» в шапке реестра проектов

6 кнопок: вкл/откл проектов, желаемое кол-во номеров, удалённые проекты, выгрузка источников, управление страницей, восстановление/удаление.

Решение для Лидерры:

  • Биз-16 → новое поле tenants.desired_daily_numbers INT NULL (см. §3.5.3). Сигнал саппорту, отображается в админке SaaS, не влияет на биллинг и effective_daily_limit.
  • Биз-14 → soft-delete проектов с TTL (см. §3.5.4). Дефолт 6 месяцев + cron disabled.
  • Массовые действия (вкл/откл, удалить, восстановить) — стандартный bulk-паттерн в админ-UI клиента.

9.2.3. «Просмотреть» — это та же карточка проекта

«Просмотреть» — это aria-label на pencil-иконке для скринридеров. Открывается та же модалка «Редактировать проект». Отдельной сущности project_pending_numbers не нужно.

9.2.4. Жёсткое подтверждение модели B1/B2/B3 — capabilities

Цитата из карточки проекта оригинала (партия 13.3.5):

«Указать в связке 'Наименование отправителя' и 'Ключевое слово' можно только по поставщику B2. Поставщик B3 работает только по наименованию отправителя».

Это требует расширения suppliers 5 полями (см. §3.5.2). В UI Лидерры: при выборе поставщика(ов) frontend показывает только релевантные поля (intersection capabilities).

9.2.5. «Список звонков» — только агрегация

Раздел /admin/user/moizvonki показывает агрегацию по менеджерам. Сквозного журнала отдельных звонков нет. Drill-down не предоставлен.

Решение для Лидерры: подтверждает Биз-12 — телефонию не делаем на MVP. Звонки конкретной сделки — только в карточке сделки.

9.3. Партия 14 — Редкие статусы + KB + Безопасность профиля

9.3.1. Карточка сделки во всех 14 статусах — единая

Эмпирически проверить не удалось (на текущем аккаунте 0 сделок во всех 14 статусах). Косвенно подтверждено через DOM фильтра — единый плоский список из 14 статусов без условных подразделов.

Решение для Лидерры: одна универсальная карточка сделки, без условных полей по статусу.

9.3.2. База знаний — внешняя (HelpDeskEddy)

Встроенной KB в CRM нет. В левом меню «База Знаний» — это <a href> на data.helpdeskeddy.com. Чат-виджет — <iframe> HelpDeskEddy.

Решение для Лидерры: на MVP линкуем «База знаний» на наш JivoSite KB / Notion. Встроенную KB не делаем. Чат — JivoSite (Биз-5 ).

9.3.3. Безопасность профиля оригинала

Что есть: username, password, email, notify_email, timezone_id, language_id, имя/фамилия/телефон/мобильный, окно «не беспокоить» (start_hour 0..23 + end_hour 0..23, минимум 3 часа).

Чего нет: 2FA, журнал входов, список активных сессий, recovery codes, история смены пароля, IP allow-list.

🔴 Уязвимости оригинала:

  • Поле «Текущий пароль» — <input type="text"> с заполненным plaintext-значением. Анти-паттерн.
  • Смена пароля inline в общем POST-запросе формы профиля — анти-паттерн.

Формат «Тихих часов» оригиналаstart_hour 0..23 + end_hour 0..23 + общий timezone_id + минимум 3 часа.

Решение для Лидерры:

  • users.notification_preferences.quiet_hours{enabled, from_hour: 0..23, to_hour: 0..23} + привязка к users.timezone. Минимум 3 часа интервал (паритет, server-side check).
  • Расширения сверх паритета (Post-MVP): минуты (HH:MM), per-channel quiet hours, исключения по дням недели.

Дополнительный экран профиля Лидерры «Безопасность» (отдельная вкладка) — наше конкурентное преимущество над оригиналом:

  • Раздел «Пароль» — отдельная защищённая форма old/new/confirm.
  • Раздел «Двухфакторная аутентификация» — TOTP setup, recovery codes, отключение.
  • Раздел «История входов» — таблица из auth_log. Self-service.
  • Раздел «Активные сессии» — таблица из user_sessions с кнопкой «Завершить».
  • Раздел «Уведомления о подозрительной активности» — toggle email-уведомлений.

9.4. Партия 15 — Карточки внешних интеграций «API ▾» (окончательный вердикт по webhook'ам)

9.4.1. Полный список интеграций «API ▾»

12 пунктов в 6 категориях: CRM (Битрикс, amoCRM), Email/маркетинг (Unisender, Google таблица), Телефония (Скорозвон, Мои Звонки, Mango Office), Голосовой робот (с подменю), AI/утилиты (Chat GPT, Проверка репутации номеров), Прочее (API, Подписаться на Телеграм).

Категория «Рекламные кабинеты» (Я.Директ, ВК Ads) — отсутствует. Потенциальная категория для Post-MVP roadmap.

9.4.2. Окончательный вердикт по outbound webhook'ам

В оригинале outbound webhook subsystem НЕТ. 7 независимых линий доказательств:

Линия Партия Результат
Карточка проекта 10 Нет полей webhook
Форма создания проекта 10 Нет полей webhook
История действий проекта 5 мес 10 Нет логов webhook
WebSocket / SSE 9 0 каналов
24 webhook-keyword в DOM 10 0 совпадений
Список «API ▾» (12 пунктов) 15.1 Ни одного с webhook-функционалом
Открытые карточки 5 категорий 15.2 0/5 редактируемых полей «outbound webhook URL»
JS-код по 31 keyword 15.3 0 совпадений в кастомном JS (~71 КБ)
Network-мониторинг 15.4 0 внешних XHR; все вызовы провайдер-API — серверной стороной

Уточнение: в HTML карточки Bitrix24 найден read-only URL https://prostats.info/bitrix24/webhook.php?id={{formData.id}}. Это inbound-канал (Bitrix → prostats.info → CRM серверным путём): prostats.info — отдельная инфраструктура группы. Это НЕ generic outbound webhook subsystem CRM.

Архитектурный паттерн всех интеграций оригинала: outbound API-client. CRM аутентифицируется в провайдере по credentials и пушит данные через provider-specific REST.

Решение для Лидерры: наш Уровень 1 (outbound webhook на MVP, OPEN-И-2) — окончательно конкурентное преимущество. Маркетинг: «отдаём лиды в любую внешнюю систему по webhook'у — у конкурентов нет».

9.4.3. Структура карточек интеграций (для §4.5 narrative)

Все карточки — отдельные страницы, единая схема: Статус toggle + блок credentials (провайдер-специфичный) + блок маппинга полей + блок маппинга статусов. Кнопки: «Создать» (Yii) или «Далее/Отмена/Удалить» (Vue SPA).

🔴 Уязвимости оригинала (партия 15.2): все credentials хранятся в <input type="text">, не password. Видны на экране.

Решение для Лидерры: при реализации интеграций (Уровень 2 — amoCRM в спринте 14–15) использовать ту же структуру с двумя обязательными отличиями:

  1. Все credentials — masked/password type + toggle «👁 Показать».
  2. Outbound webhook fallback: даже для коннекторов с провайдер-API оставить опцию «отдавать в webhook вместо провайдер-API».

9.5. Новые продуктовые вопросы (Биз-14/15/16)

Биз-14 — TTL soft-delete проектов

Контекст партии 13.2.3: оригинал хранит soft-deleted проекты ≥3 месяца. Точный TTL не определён.

Рекомендация Claude: 6 месяцев дефолтом + cron disabled (projects_purge_deleted_enabled = false). Compliance с 152-ФЗ ст.5 п.7. Включается админом SaaS вручную после согласования с юристом.

Дефолт при отсутствии решения: 6 месяцев + cron disabled.

Биз-15 — Wizard «Рекомендации источников» (OSINT)

Контекст партии 13.1: 3-шаговый wizard для поиска новых доменов/телефонов конкурентов.

Рекомендация Claude: На MVP НЕ делаем, Post-MVP по запросу. Целевая аудитория Лидерры — арбитражники с готовыми источниками.

Дефолт при отсутствии решения: не делаем на MVP.

Биз-16 — Поле desired_daily_numbers на тенанте

Контекст партии 13.2.2: аккаунтная настройка «Желаемое кол-во номеров в день» — целевой ориентир для саппорта.

Рекомендация Claude: Делаем на MVP — поле tenants.desired_daily_numbers INT NULL + UI в кабинете клиента (Профиль) + отображение в админке SaaS.

Дефолт при отсутствии решения: делаем на MVP.

9.6. Дополнения в narrative для v8.4 (по итогам партий 12–15)

В дополнение к §4.1–§4.10 (по итогам партий 1–11), для v8.4 добавляются дополнения в:

  • §7 (Сущности и схема БД) — описание reminders v8.3 и удаление полей deals.reminder_text/reminder_at. Расширение suppliers capabilities.
  • §8 (Воронка продаж и статусы) — пункт про единую универсальную карточку сделки во всех 14 статусах.
  • §9 (Мои Проекты) — раздел про soft-delete с TTL (Биз-14), массовые операции, аккордеон «Настройки».
  • §12 (Дашборд и аналитика) — точная структура отчёта «Конверсия проектов»: 17 колонок, формат N (XX.XX%), monotone per column, total-row, экспорт XLSX (наше расширение), drill-down (наше расширение).
  • §17 (Напоминания) — полная переписка раздела: множественные напоминания на сделку (новая reminders), фильтры ?reminders=today|last|future|none, минимальный набор полей паритета.
  • §18.5 (Профиль) — новая вкладка «Безопасность» (Пароль / 2FA / История входов / Активные сессии / Подозрительная активность). Формат «Тихие часы» паритета.
  • §19 (REST API) — раздел про outbound webhook как наше уникальное конкурентное преимущество (7 линий доказательств).
  • §22 (Безопасность) — расширение раздела про prompt injection. Plus: антипаттерн «password в <input type="text">».
  • §23.10 (Админка SaaS)tenants.desired_daily_numbers для саппорта.
  • §26 (Дизайн-система) — раздел про маскирование credentials в формах интеграций.

10. Версионирование

v1.0 (04.05.2026) — исходное приложение по итогам аудита crm.bp-gr.ru от 04.05.2026 (11 партий).

v1.1 (05.05.2026) — расширение по итогам параллельного аудита партий 12–15 от 05.05.2026:

  • Новый раздел 9 «Результаты партий 12–15».
  • Биз-10 переоткрыт (модель напоминаний — массив, не одиночное поле).
  • Окончательный вердикт по outbound webhook'ам (нет, 7 линий доказательств).
  • Уточнена модель suppliers — capabilities B1/B2/B3.
  • 3 новых вопроса (Биз-14/15/16).
  • Расширен §3 — патчи schema.sql v8.3 (§3.5).
  • Обновлены §6 (безопасность) и §7 (итоговая сводка).
  • Обновлён TL;DR.

Будущие версии:

  • При следующих аудитах оригинала (например, если откроется биллинг) — обновляется как v1.2, v1.3 и т.д.
  • При фиксации ответов заказчика на Биз-10/11/12/13/14/15/16 — разделы 5 и 9.5 переписываются с пометкой «закрыто».
  • При полной интеграции в narrative v8.4 — Прил. М становится историческим документом (не удаляется, остаётся как обоснование архитектурных решений).

Конец Приложения М v1.1.