| Открытые вопросы по дизайну | `Открытые_вопросы_v8_3.md` Диз-1, Диз-3 |
@@ -86,7 +86,7 @@ lidpotok/
| [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) | Прил. Н v1.0 — полный реестр 28 инструментов в 4 фазах (фаза 0 — сейчас, +1 Laravel, +2 Vue, +3 pre-prod), конфликты и решения, процедура перехода между фазами, особенности Windows + PowerShell |
| [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) v1.2 | Продуктовые правила работы Claude в проекте |
| [docs/README_АРХИВ_v8_4.md](docs/README_АРХИВ_v8_4.md) v8.4 | Состав архива, навигатор по документам |
| [docs/CRM_bp-gr_Инструкция_v8_5.md](docs/CRM_bp-gr_Инструкция_v8_5.md) v8.5 | Главное ТЗ из 28 разделов (v8.5 — реализация 27 решений аудита C от 07.05.2026; v8.4 финал был 06.05.2026) |
| [db/schema.sql](db/schema.sql) v8.5 | Схема БД PostgreSQL 16 (54 таблицы + 12 партиций, 91 индекс, 34 RLS-политики, 4 роли, 12 триггеров, 4 функции — после v8.5 от 07.05.2026) |
# Лидпоток — Полная техническая и функциональная документация (v8.4)
# Лидпоток — Полная техническая и функциональная документация (v8.5)
> **Версия:** 8.4 от 06.05.2026 (финал, все 13 разделов плана переписаны).
> **Базовая версия:** 8.3 от 04.05.2026.
> **Версия:** 8.5 от 07.05.2026 (реализация 27 решений аудита C из реестра v1.12).
> **Базовая версия:** 8.4 от 06.05.2026 (финал, все 13 разделов плана переписаны).
> **Рабочее название продукта:****Лидпоток**.
> **Статус:** Полное ТЗ для разработки SaaS-платформы. **41 продуктовое решение зафиксировано** (30 в v8.3 + 11 в v1.10 реестра 06.05.2026). См. блок «Что нового» ниже.
> **Готовность:** 100% по архитектурным решениям; брендинг готов (см. brandbook.md v1.1). Все P2 закрыты по дефолтам. Истинных P0-блокеров остался **1**: Б-1 (реквизиты юр. лица, ждут ООО).
> **Статус:** Полное ТЗ для разработки SaaS-платформы. **68 продуктовых решений зафиксировано** (30 в v8.3 + 11 в v1.10 + 27 в v1.12). См. блок «Что нового в v8.5» ниже.
> **Готовность:** 100% по архитектурным решениям; брендинг готов (см. brandbook.md v1.1). Все P2 закрыты повторно (v1.12). Истинных P0-блокеров остался **1**: Б-1 (реквизиты юр. лица, ждут ООО).**8 P0 аудита C закрыты в v1.12** — триггер фазы 1 (`composer create-project`) разблокирован архитектурно.
> **Структура:** этот файл — основной narrative + **11 приложений** + **brandbook.md v1.1** + **Прил. Н (Tooling_v8_3.md)** + **корневой CLAUDE.md** + **дизайн-бриф для дизайнера** (см. раздел 28).
## Что нового в v8.5 относительно v8.4 (07.05.2026)
> Этот блок описывает правки v8.4 → v8.5. Источники: реестр v1.12 §13.10 (27 закрытий аудита C от 07.05.2026), schema.sql v8.5 (коммит `038a884`), CHANGELOG_schema.md §Y.
- **Биз-17 (см. §10.X ниже)** — `projects.assignment_strategy VARCHAR(32) DEFAULT 'manual'` + CHECK `IN ('manual','round_robin','least_loaded')`. MVP = manual (паритет с оригиналом). Round-robin/least-loaded зарезервированы для Post-MVP — реализация через `project_user_assignments` (CTO-16) + cron-балансировку.
- **Биз-18 (§12.5.5)** — TTFR-SLA: `projects.ttfr_target_minutes INT DEFAULT 15`. Алерт при просрочке через event bus + UI badge на карточке. Default 15 мин — комфортный baseline для pay-per-lead-сегмента.
- **Биз-19 (§10.X)** — антифрод-дедуп по phone в окне 24 ч. `deals.duplicate_of_id BIGINT` (без FK — партиционированная таблица), дубль помечается, **НЕ списывается с баланса**.
- **CTO-13 (§22.X + Прил. И)** — обязательный e2e-тест `SET LOCAL app.current_tenant_id` через PgBouncer transaction-pooling в спринте 1. Без прохождения теста (auto-commit, reuse соединения, job retry) триггер фазы 1 не открывается.
- **OPEN-И-13 (§22.X + §23.10.X)** — Yandex 360 SSO для админов SaaS: OIDC + JIT-provisioning. Локальный 2FA выключен (заменён на 2FA провайдера IDP). Fallback — break-glass-аккаунт `super_admin`с force-2FA на случай недоступности IDP. `saas_admin_users.sso_provider` + `saas_admin_users.is_break_glass`.
- **OPEN-И-14 (§22.6)** — defense-in-depth: WITH CHECK на политики `deal_tag_pivot` и `saas_invoice_items` (закрывает INSERT-обход RLS) + REVOKE ALL на 6 saas-таблицах от `crm_app_user` (`saas_admin_users`, `saas_admin_sessions`, `saas_admin_audit_log`, `incidents_log`, `pd_subject_requests`, `impersonation_tokens`).
- **OPEN-И-15 (§14.X + §22.8 + Прил. И)** — append-only audit hash chain. `log_hash BYTEA` на 5 audit-таблицах, 10 триггеров (5 BEFORE INSERT для `audit_chain_hash()` + 5 BEFORE UPDATE/DELETE для `audit_block_mutation()`), новая роль `crm_audit_writer` (только INSERT). Cron `audit:verify-chain` пересчитывает SHA-256 chain раз в сутки.
- **OPEN-И-16 (§22.X)** — Sentry PII-scrubbing: whitelist + regex маска `phone/email/password/secret/token/api_key` во всех контекстах (request, breadcrumbs, exception args). Конфигурация в Laravel `config/sentry.php``before_send`. Закрывает 152-ФЗ ст.6 риск.
**12 P1 — реализация в фазах 1–2:**
- **Биз-20 (§17.X)** — Telegram-бот для нотификаций менеджерам в спринте 9. `users.telegram_user_id` + `tenants.telegram_bot_token` (зашифровано). Подключение через `/start` команду.
- **Биз-21 (§19.10.X)** — generic outbound `marketing.conversion` event для интеграций с Я.Директ Offline + VK Ads Goals. Расширение whitelist событий §19.10.2 без новой таблицы.
- **Биз-22 (§10.X)** — простой lead-scoring: `lead_score = supplier.quality_score × (time_in_form_seconds / 60)`, clamped в `[0, 99.99]`. Без ML. Триггер `calc_lead_score()` BEFORE INSERT/UPDATE на `deals`.
- **CTO-14 (§12.5.5)** — UTM-поля для когортной аналитики: `deals.utm_source/utm_medium/utm_campaign/utm_content` + индекс `(tenant_id, utm_source)`.
- **CTO-15 (§22.X + §23.10.X)** — two-person impersonation для тенантов с `pd_subject_request.processing_restricted=TRUE` ИЛИ `chargeback_unrecovered_rub > 0`. `impersonation_tokens.second_approver_id` + `second_approval_at` (роль `compliance` обязательна).
- **CTO-16 (§10.X)** — `project_user_assignments(project_id, user_id, skills JSONB)` — пул менеджеров проекта для `assignment_strategy IN ('round_robin','least_loaded')` Post-MVP.
- **OPEN-И-17 (§22.X + Прил. И)** — TTL 365 дней на API-ключах. Cron `secrets:notify-expiring` уведомляет за 30 дней до expiry. UI «продлить» ставит `+365d`.
- **OPEN-И-18 (§19.10.X)** — DNS-rebinding защита для outbound webhook: resolve→pin IP→connect, blacklist RFC1918 на pinned IP, ≤3 redirect, max body 1 MB.
- **OPEN-И-19 (§19.3)** — hard limit 10 ключей: `tenants.api_key_limit INT DEFAULT 5` + `api_keys.expires_at NOT NULL DEFAULT NOW() + 365d`. Защита от DoS-через-ключи.
- **Ю-9 (§22.X)** — hard-блок impersonation для всех ролей кроме `compliance` при `pd_subject_request.processing_restricted=TRUE` (152-ФЗ ст.21 ч.5). Реализация совместно с CTO-15 — единый guard в `SaasAdminAuthService`.
**7 P2 — реализация в фазах 1–3:**
- **Биз-23 (§10.X)** — `deals.region_code VARCHAR(8)` + `deals.city VARCHAR(100)` + автоопределение по prefix phone через `PhonePrefixService`.
- **Биз-24 (§17.X)** — алерт через 48 ч в админке + email finance при просрочке `waiting_payment` → `paid` (cron `payments:notify-stale`).
- **OPEN-И-22 (§23.7 + Прил. И)** — per-tenant DEK в Yandex KMS, encryption envelope для бэкапов. Crypto-shred при удалении тенанта (destroy DEK).
- **OPEN-И-23 (§22.8)** — роль `crm_audit_writer` (write-only) уже создана в OPEN-И-15. Здесь — отдельный пункт реестра для прослеживаемости.
- **OPEN-И-24 (Прил. И)** — документированная процедура `pg_dump prod → pg_anonymizer → restore staging`. Автоматизация в CI. Расширение `pg_anonymizer` ставится в фазе 3 (Прил. Н).
- **OPEN-И-25 (§10.X + §17.X)** — cron `leads:escalate-stale` каждые 30 мин: если `deals.assigned_at + 4h < NOW()` AND status NOT IN (closed, rejected) → reassign + email + `escalated_count++`.
- **OPEN-И-26 (§10.4 + schema)** — закомментированный DDL `call_recordings(...)` (10 строк) в `db/schema.sql` — задел под Биз-12 Post-MVP.
- **Прил. И (Runbook)** — обновлён по OPEN-И-21/22/24/25, CTO-13.
## Что нового в v8.4 относительно v8.3 (06.05.2026)
> Этот блок описывает правки v8.3 → v8.4, выполненные 06.05.2026 в одну сессию. Все 13 разделов плана v8.4 переписаны. Источники: реестр v1.10, Прил. М v1.1, брендбук v1.1, schema.sql v8.4.
@@ -1236,12 +1288,13 @@ CREATE TABLE import_log (
**Уровень тенанта (с `tenant_id`):**
- `users` — пользователи (`notification_preferences` JSONB вместо 4 BOOLEAN в v8.1, CTO-4)
- `users` — пользователи (`notification_preferences` JSONB вместо 4 BOOLEAN в v8.1, CTO-4; **+ `telegram_user_id` в v8.5, Биз-20**)
- `projects` — проекты-источники лидов (+ 6 полей в v8.2, партии 10.3/10.6: `daily_limit_target`, `effective_daily_limit_today`, `effective_limit_calculated_at`, `region_mask`, `region_mode`, `delivery_days_mask`)
- `projects` — проекты-источники лидов (+ 6 полей в v8.2, партии 10.3/10.6: `daily_limit_target`, `effective_daily_limit_today`, `effective_limit_calculated_at`, `region_mask`, `region_mode`, `delivery_days_mask`; **+ `assignment_strategy`/`ttfr_target_minutes` в v8.5, Биз-17/18**)
- `project_suppliers` — m2m связь проектов с поставщиками (v8.2, партия 10)
- **`project_user_assignments`** — m2m «проект ↔ менеджеры» с per-assignment skills для skill-based routing (v8.5, CTO-16; используется при `assignment_strategy IN ('round_robin','least_loaded')` Post-MVP)
- `project_limit_adjustments` — ручные корректировки лимитов проекта (v8.2, партия 10.7)
- `deals` — сделки (партиционирована по `received_at`; в v8.3 удалены `reminder_text/reminder_at` — партия 12.2.5)
- `deals` — сделки (партиционирована по `received_at`; в v8.3 удалены `reminder_text/reminder_at` — партия 12.2.5; **+ 11 полей в v8.5: `duplicate_of_id` Биз-19, `utm_source/medium/campaign/content` CTO-14, `region_code/city` Биз-23, `time_in_form_seconds/lead_score` Биз-22, `assigned_at/escalated_count` OPEN-И-25**)
- `balance_transactions` — транзакции лидов (+ `chargeback_writedown`, `chargeback_repayment` в v8.1, Ю-3; **+ `log_hash` в v8.5, OPEN-И-15**)
- `api_keys` — ключи исходящего REST API (**v8.5: `expires_at` теперь NOT NULL, default NOW()+365d, OPEN-И-17/19**)
- **`outbound_webhook_subscriptions`, `outbound_webhook_deliveries`** — подписки тенантов на исходящие события + журнал доставок (v8.4, см. [§19.10](#1910-outbound-webhook-подписка-внешних-систем-на-события))
- `auth_log` — лог входов в систему (расширен в v8.1: `actor_type` для объединения tenant_user и saas_admin)
- `auth_log` — лог входов в систему (расширен в v8.1: `actor_type` для объединения tenant_user и saas_admin; **+ `log_hash` в v8.5, OPEN-И-15**)
- `activity_log` — журнал действий (**+ `log_hash` в v8.5, OPEN-И-15**)
- `saas_admin_audit_log` — журнал действий админов
- `impersonation_tokens` — одноразовые коды «Войти как клиент» (Ю-1)
- `saas_admin_audit_log` — журнал действий админов (**+ `log_hash` в v8.5, OPEN-И-15**)
- `impersonation_tokens` — одноразовые коды «Войти как клиент» (Ю-1; **+ `second_approver_id`/`second_approval_at` в v8.5 для two-person approval, CTO-15+Ю-9**)
**152-ФЗ:**
- `tenant_consents` — согласия на обработку ПДн (+ `consent_type='pd_processing_as_data_provider'` в v8.1)
- `pd_processing_log` — журнал обработки ПДн
- `pd_processing_log` — журнал обработки ПДн (**+ `log_hash` в v8.5, OPEN-И-15**)
- `pd_subject_requests` — обращения субъектов ПДн (новая в v8.1; + `processing_restricted` BOOLEAN в v8.2 OPEN-Д-1)
- `incidents_log` — журнал инцидентов SaaS (новая в v8.2, OPEN-Д-5/И-1)
@@ -1281,9 +1335,11 @@ CREATE TABLE import_log (
- `email_verifications`, `password_resets`
- `failed_jobs`
> **Полный DDL** всех 53 таблиц + 12 партиций со всеми constraint, индексами и RLS-политиками — в [Приложении А — `schema.sql` v8.4](../db/schema.sql). История изменений — [`db/CHANGELOG_schema.md`](../db/CHANGELOG_schema.md) (записи Z=v8.4, A=v8.3, B=v8.2).
> **Полный DDL** всех 54 таблиц + 12 партиций со всеми constraint, индексами и RLS-политиками — в [Приложении А — `schema.sql` v8.5](../db/schema.sql). История изменений — [`db/CHANGELOG_schema.md`](../db/CHANGELOG_schema.md) (записи**Y=v8.5**, Z=v8.4, A=v8.3, B=v8.2).
>
> **Что отложено в schema (план §7 v8.4 упоминал, но НЕ добавлено):**`crm_connections` и `crm_field_mappings` (Уровень 2 интеграций, OPEN-И-2) — DDL появится в v8.5+ при старте реализации amoCRM-коннектора в спринте 14–15. До этого момента Уровень 1 (`outbound_webhook_*`) закрывает потребность.
> **Метрики schema.sql v8.5:** 54 таблицы + 12 партиций; 91 индекс; 35 RLS-политик (из них 2 с WITH CHECK на INSERT); 35 ENABLE ROW LEVEL SECURITY; 4 роли БД (`crm_app_user`, `crm_admin_user`, `crm_migrator`, **`crm_audit_writer` v8.5**); 12 триггеров (5×2 audit append-only + 1 report_jobs export-log + 1 deals lead_score); 4 функции (`audit_chain_hash`, `audit_block_mutation`, `report_jobs_log_export`, `calc_lead_score`).
>
> **Что отложено в schema (план упоминал, но НЕ добавлено):**`crm_connections` и `crm_field_mappings` (Уровень 2 интеграций, OPEN-И-2) — DDL появится в v8.6+ при старте реализации amoCRM-коннектора в спринте 14–15. До этого момента Уровень 1 (`outbound_webhook_*`) закрывает потребность. `call_recordings` (Биз-12 Post-MVP, OPEN-И-26) — закомментированный задел в `db/schema.sql` секция 17.
## 7.2. ER-диаграмма
@@ -2047,6 +2103,109 @@ URL: `/deals/create`
При создании ручной сделки `source_crm_id = NULL`, `is_test = false`. **Баланс не списывается** — ручные сделки бесплатны (это управленческое решение, можно изменить через настройку `system_settings.charge_manual_deals`).
### 10.8.1. Антифрод дублей по `phone` в окне 24 ч (Биз-19)
**Проблема:** в pay-per-lead-сегменте поставщик может прислать одного физлица дважды (двойной submit формы / повторный звонок) — без защиты клиент платит за оба. Без дедупликации продукт не отличим от голого webhook'а.
**Решение:**
- При входящем webhook (`ProcessWebhookJob` §5.5) сервис `DuplicateDetector` ищет в `deals` записи с тем же `(tenant_id, phone)`, у которых `received_at >= NOW() - INTERVAL '24 hours'`.
- Если найден master (запись без `duplicate_of_id`) — новой сделке проставляется `duplicate_of_id = master.id` и **списания с `tenants.balance_leads/balance_rub` НЕ происходит** (`balance_transactions.type` НЕ создаётся).
- Дубль виден в карточке сделки master'а как «Связанные дубли» (UI list по `(duplicate_of_id = master.id)`).
- Клиент видит метку «дубль за 24 ч» в списке `/deals` и фильтре «исключая дубли».
- **Окно фиксированное 24 ч** (не настраивается на MVP). Если поставщик задерживает повторку — клиент платит. Это компромисс между антифродом и легитимными повторными интересами.
**Lookup performance:** существующий индекс `(tenant_id, phone)` достаточен — 24 ч мелкая выборка, app-уровневый фильтр по `received_at` дешёв.
- M2M «проект ↔ менеджеры» с per-assignment skills (slug-и: `['it_b2b','retail','sms_NAME1']`).
- Скилы используются только при `assignment_strategy='least_loaded'` для статистики; полный skill-based routing (intersection projects.required_skills × users.skills) — Post-MVP-расширение.
- При assignment_strategy='manual' (MVP) таблица не используется — UI скрывает соответствующую вкладку проекта.
**Реализация на MVP:** schema готова (см. `db/schema.sql` v8.5), но cron lead-router не пишется. Менеджер по-прежнему выбирается вручную. Включение Post-MVP — однострочный feature-flag.
- `suppliers.quality_score NUMERIC(3,2) DEFAULT 1.00` — настраивается админом в `/admin/suppliers` по фактической конверсии за месяц. Post-MVP — auto-adjustment по cron.
- `deals.time_in_form_seconds INT NULL` — приходит из webhook payload как `seconds_to_submit` (если поставщик передаёт; иначе NULL → `lead_score = NULL`).
**UX:** badge на карточке сделки (зелёный 🔥 ≥3.0 / жёлтый 1.0–3.0 / без badge <1.0 или NULL). В Kanban (§11) — сортировка колонки «Новые» по lead_score DESC опциональна.
**Полная скоринг-модель с обучением на исторических данных** (Биз-22 вариант B) — Post-MVP, требует ≥6 месяцев накопления данных.
### 10.8.4. Гео-таргетинг сделок (Биз-23)
**Поля:**
- `deals.region_code VARCHAR(8)` — ISO 3166-2:RU (`RU-MOW`, `RU-SPE`, `RU-MOS` и т. д.).
- `deals.city VARCHAR(100)` — свободный текст (приходит из webhook или enrichment-сервиса).
**Автоопределение `region_code`:** сервис `App\Services\Geo\PhonePrefixService` использует offline-CSV префиксов Минсвязи РФ (~5 МБ, обновление 1×/год). Если prefix не найден — `region_code = NULL`, `city` остаётся как есть.
**Использование:**
- **Фильтр в `/deals`** (§10.3) — мультиселект регионов.
- **Когорты в §12.5.5** — конверсия по региону.
- **Не путать с `projects.region_mask`** (битмаска ФО для приёма webhook): `projects.region_mask` ограничивает inbound на уровне проекта, `deals.region_code` — атрибут сделки для аналитики/фильтра.
**Cron `leads:escalate-stale`** — каждые 30 мин (`schedule.php`).
**Логика:**
```sql
SELECT id, tenant_id, project_id, manager_id
FROM deals
WHERE assigned_at IS NOT NULL
AND assigned_at + INTERVAL '4 hours' < NOW()
AND status NOT IN ('closed','rejected','spam')
AND escalated_count < 3 -- защита от бесконечного reassignment
LIMIT 100;
```
Для каждого:
1. `escalated_count++`.
2. Reassign на следующего active member проекта (если `assignment_strategy != 'manual'`) или supervisor проекта (если `manual`).
3. Email старому + новому менеджеру + админу тенанта.
4. Запись в `activity_log` event=`deal.escalated`с`context = {old_manager_id, new_manager_id, escalation_count}`.
**Связь с TTFR (Биз-18, см. §12.5.5):** TTFR = «время от `received_at` до первого человеческого касания». Эскалация = «менеджер забыл про назначенный лид». Это разные метрики, но дополняющие.
**Индекс:** `(tenant_id, assigned_at) WHERE status NOT IN ('closed','rejected')` — для cron.
### 10.8.6. Архитектурный задел под call-recording (OPEN-И-26)
**Поля:** `tenant_id, deal_id, call_started_at, duration_sec, direction (in/out), recording_path (S3), transcript`.
**Интеграция с pd_processing_log:** при доступе к `recording_path` через signed URL — обязательная запись `action='exported'` (как в OPEN-И-20 для `report_jobs`).
---
# 11. Kanban-доска
@@ -2251,9 +2410,29 @@ GROUP BY day_of_week, hour;
Таблица: проекты с сортировкой по конверсии, объёму лидов, выручке (если интеграция с биллингом).
### 12.5.5. Производительность менеджеров
### 12.5.5. Производительность менеджеров и TTFR-SLA (v8.5: Биз-18)
Таблица: менеджер | сделок принято | переведено в paid | конверсия | средний срок до закрытия.
**Базовая таблица:** менеджер | сделок принято | переведено в paid | конверсия | средний срок до закрытия.
**v8.5 — Time To First Response SLA (Биз-18):**
- **Метрика TTFR** = время от `deals.received_at` до первого человеческого касания (изменения `status`с`new` на любой другой ИЛИ создания `activity_log` event=`deal.viewed`/`deal.commented` в карточке).
- **`projects.ttfr_target_minutes`** (default 15, диапазон 1–1440) — настраивается в карточке проекта `/projects/:id/edit`. Применяется ко всем сделкам проекта.
- **Алерт при просрочке:**
1. Через `ttfr_target_minutes` после `received_at` — если deal остаётся в статусе `new` без касаний — событие `deal.ttfr_breached` через event bus → email + push менеджеру + email админу тенанта.
2. UI badge на карточке сделки: серый «⏱ 12 мин» (норма) → жёлтый при >50% (`12 мин > 7.5 мин при target 15`) → красный «⏱ 18 мин ⚠️ просрочка».
3. Метрика «TTFR p50 / p95» в дашборде §12.5.5 рядом с конверсией.
- **Связь с эскалацией (OPEN-И-25, см. §10.8.5):** TTFR — про первое касание, эскалация — про забытые лиды. Жёсткие 5-минутные SLA (Биз-18 вариант B) не выбраны — риск выгорания менеджеров и ложных эскалаций для pay-per-lead.
**v8.5 — UTM-когортная аналитика (CTO-14):**
Поля `deals.utm_source`, `utm_medium`, `utm_campaign`, `utm_content` (4 × VARCHAR(100) NULL) приходят из webhook payload (если поставщик передаёт в `extras.utm_*`) или landing page при manual-create. Индекс `(tenant_id, utm_source)` partial WHERE NOT NULL.
Когортные отчёты в `/analytics/cohorts`:
- **Конверсия по `utm_source`** — таблица source × conversion_rate × avg_lead_score (Биз-22) × ROI (если `marketing.conversion` event настроен в §19.10, Биз-21).
- **Динамика `utm_campaign`** — line chart за 30/90 дней.
- **`utm_content` A/B-test** — сравнение вариантов креатива при равном `utm_campaign`.
### 12.5.6. Конверсия проектов (паритет с оригиналом, партия 12.1)
@@ -2573,6 +2752,59 @@ public function updated(Deal $deal): void
**Маркетинговое позиционирование** (преимущество №2 из 6, см. [§1.4.1](#141-конкурентные-преимущества-vs-оригинал-crmbp-grru)): «Полный аудит всех изменений сделки — в отличие от оригинала, где можно незаметно перевесить лид на другого менеджера или подменить телефон».
## 14.8. v8.5: Append-only audit hash chain + журнал экспорта
**Проблема:** до v8.5 audit-журналы (`activity_log`, `auth_log`, `pd_processing_log`, `saas_admin_audit_log`, `balance_transactions`) технически могли быть изменены через прямой SQL `UPDATE`/`DELETE` под ролью `crm_app_user` (если application code допустит) или под `super_admin` SaaS. Юридически это значит, что доказательственная сила журналов слаба — недобросовестный admin мог стереть запись о своём действии.
**Решение в v8.5:**
- **`log_hash BYTEA` колонка** на 5 audit-таблицах — заполняется триггером `audit_chain_hash()` BEFORE INSERT по формуле `log_hash = sha256( prev_row.log_hash || NEW::text )`.
- **Триггер `audit_block_mutation()` BEFORE UPDATE/DELETE** — RAISE EXCEPTION 'audit log is append-only'. ALTER TABLE … DISABLE TRIGGER возможен только под суперпользователем PG (которого у админов SaaS нет в production).
- **Отдельная роль `crm_audit_writer`** (только INSERT на 5 audit-таблиц). Application пишет в audit под этой ролью через temporary `SET ROLE crm_audit_writer`. Даже если admin отключит триггеры, эта роль не имеет UPDATE/DELETE permissions.
- **REVOKE ALL UPDATE, DELETE** на 5 audit-таблиц от `crm_app_user`. Два слоя защиты (триггер + permissions).
**Cron `audit:verify-chain`** (раз в сутки в 04:00 МСК):
1. Для каждой из 5 audit-таблиц проходит по `id ASC`.
3. Сравнивает с`row.log_hash`. При расхождении — алерт в Sentry severity=critical + email админам SaaS + запись в `incidents_log` type=`audit_chain_break`.
4. Под нормальной работой — нулевая нагрузка (полная проверка ~5 минут на 50М строк, потом инкрементально с last-checked id).
**Юридический эффект:** журналы становятся доказательственно сильными. Любая попытка модификации после INSERT'а — либо отклонена триггером, либо обнаружена cron'ом. Ссылки в спорах — на конкретные `id` строк + `log_hash`, который математически связан со всеми предыдущими.
**Защита от ALTER TABLE … DISABLE TRIGGER:** под `super_admin` SaaS, действующим через `crm_audit_writer`, эта команда не пройдёт (нет прав на ALTER). Под `super_admin` PostgreSQL — возможна, но это эскалация привилегий, фиксируемая отдельно (root-level audit на уровне ОС).
### 14.8.2. Триггер экспорта в `pd_processing_log` (OPEN-И-20)
**Проблема:** при создании `report_jobs` (выгрузка лидов в файл) запись в `pd_processing_log` action='exported' до v8.5 делалась из application-кода. Риск пропуска при ошибке программиста или новом фичевом коде.
**Решение:**
```sql
CREATE TRIGGER trg_report_jobs_export_log
AFTER INSERT ON report_jobs
FOR EACH ROW EXECUTE FUNCTION report_jobs_log_export();
```
Функция `report_jobs_log_export()` вставляет в `pd_processing_log`:
**Эффект:** соответствие 152-ФЗ ст.18 ч.2 (фиксация всех операций обработки ПДн) обеспечивается автоматически. Пропуск audit-записи теперь невозможен — любой INSERT в `report_jobs` гарантирует запись.
**Дополнительно — signed URL для `report_jobs.file_path`:** S3 presigned URL TTL 24 ч (вместо постоянного auth-protected URL до v8.5). UI отдаёт presigned ссылку через `/api/v1/reports/:id/download`. Подробности генерации — в Прил. И (Runbook).
---
# 15. Шаблоны комментариев
@@ -2877,7 +3109,57 @@ public function handle(): void
**Что отличается от оригинала:**
- В оригинале (партия 13.2.1) — то же поведение `start_hour 0..23 + end_hour 0..23`, но **timezone настраивается на уровне пользователя**, не тенанта. У нас — **на уровне тенанта** (упрощение, общее окно для всей команды).
- Telegram-канал из оригинала **не воспроизводится** (см. §1.4) — только push + email.
- Telegram-канал из оригинала на MVP не воспроизводится. **v8.5 (Биз-20):** Telegram-бот для нотификаций менеджерам появляется в спринте 9 — см. §17.9 ниже.
## 17.9. v8.5: Telegram-канал, эскалация, late payment alert
### 17.9.1. Telegram-бот для менеджеров (Биз-20, спринт 9)
**Архитектура:**
- На уровне тенанта — **`tenants.telegram_bot_token TEXT NULL`** (зашифровано через `Crypt::encryptString`). Тенант создаёт собственного бота через `@BotFather` (1 раз) и вставляет токен в `/settings/integrations`.
- На уровне пользователя — **`users.telegram_user_id BIGINT NULL`** (Telegram chat_id). Привязка через UI: «Подключить Telegram» → бот выдаёт код → пользователь шлёт `/start <code>` → бот сохраняет `chat_id`.
**Каналы:** Telegram становится 4-м каналом (in-app/push/email/**telegram**) в матрице `users.notification_preferences` (CTO-4) — добавляется ключ `"telegram": true|false` к каждому из 8 типов уведомлений (см. §17.3). Default для существующих пользователей — `false`; включение — через `/account/notifications`.
**Реализация:** Laravel notification channel `App\Notifications\Channels\TelegramChannel`. При `notification_preferences.<type>.telegram = true` AND `users.telegram_user_id != NULL` AND `tenants.telegram_bot_token != NULL` — отправка через Telegram Bot API.
**Quiet hours (см. §17.8) применяются ко всем каналам, включая Telegram.**
**Что НЕ делаем на MVP:**
- Inbound от менеджеров через Telegram (одобрить/закрыть лид через бота). Только outbound нотификации.
При срабатывании cron `leads:escalate-stale` (см. §10.8.5) — рассылается **2 уведомления:**
1. **Старому менеджеру** (тот, у кого лид «забылся»): `notification_type='deal.escalated_from_me'` (новый тип в §17.3). Каналы по умолчанию: email + in-app. Текст: «Лид #1234 от 12.05.2026 переназначен от тебя на ИвановИИ — превышен SLA 4 часа без касания».
### 19.10.11. v8.5: DNS-rebinding защита (OPEN-И-18)
**Проблема:** до v8.5 SSRF-фильтр §19.10.10 проверял IP-адрес при добавлении подписки. Между «проверка при создании» и «реальный отправка через 5 минут» атакующий мог поменять DNS-запись `evil.example.com` с публичного IP (прошёл фильтр) на `127.0.0.1` (внутренний сервис). Это классический DNS-rebinding.
**Решение в v8.5:**
`App\Services\Webhook\SSRFGuard` при каждой отправке делает:
**Новый event type** `marketing.conversion` — для интеграций с Я.Директ Offline + VK Ads Goals (классические pixel-back события).
**Whitelist в §19.10.3 расширяется:**
```
deal.created
deal.status_changed
deal.manager_changed
deal.commented
deal.tag_added
deal.tag_removed
deal.deleted
deal.restored
+ marketing.conversion ← v8.5
```
**Когда генерируется:** при переходе `deals.status` в финальный статус-конверсия (`paid`, `worked_done` или кастомный, помеченный `lead_statuses.is_conversion=TRUE` — это новое поле, default FALSE; пока на MVP только `paid` помечен флагом). Application code в `DealStatusChangedListener` формирует event и шлёт через `OutboundDispatcher`.
**Payload:**
```json
{
"event": "marketing.conversion",
"delivery_uuid": "...",
"deal_id": 1234,
"tenant_id": 567,
"received_at": "2026-05-12T10:30:00Z",
"converted_at": "2026-05-15T16:45:00Z",
"utm": {
"source": "yandex",
"medium": "cpc",
"campaign": "spring2026",
"content": "ad_variant_b"
},
"phone_hash": "sha256:abcdef…", ← хеш для дедупликации в Я.Директ
"conversion_value_rub": 50.00 ← lead_cost из tariff_plans
}
```
**Использование на стороне клиента:**
- **Я.Директ Offline:** клиент настраивает приёмник в виде Yandex.Direct Offline Conversion API endpoint, мапит `phone_hash` → `ClientId`.
- **VK Ads Goals:** аналогично через VK Marketing API.
- **Generic adapter:** клиент может писать любой свой обработчик. **Нативные коннекторы (Биз-21 вариант B) — не делаем на MVP** — клиент сам пишет адаптер.
- [ ] В`activity_log` не утекает значение секрета?
- [ ] Rate-limit настроен (если форма отправляется на изменение секретного значения)?
## 22.13. v8.5: SSO + SET LOCAL test + Sentry PII + Anti-DDoS + TTL secrets
> **Источник:** реестр v1.12 §13.10 (P0 OPEN-И-13/14/15/16 + CTO-13; P1 OPEN-И-17/21 + CTO-15 + Ю-9). Это сводная секция изменений безопасности v8.5 — для углублённой деталировки RLS см. также §22.6, для Audit hash chain — §14.8.1, для admin SaaS workflow — §23.10.
### 22.13.1. Yandex 360 SSO для админов SaaS (OPEN-И-13)
**Решение DO-5 (04.05.2026):** Yandex 360 SSO для админов SaaS. **v8.5 (OPEN-И-13):** полный flow.
**Архитектура:**
- **Протокол:** OIDC (OpenID Connect). Yandex 360 — IDP, наш `SaasAdminAuthService` — RP (Relying Party).
- **Provisioning:****JIT** (Just-In-Time). При первом входе через Yandex 360 — автоматическое создание `saas_admin_users`с`sso_provider='yandex360'`, role=`read_only` по умолчанию. Повышение роли — вручную через `super_admin` в `/admin/admins/:id`.
- **Локальный 2FA выключен** при `sso_provider='yandex360'` — Yandex 360 имеет собственный enforced 2FA (TOTP/Yandex Key). Поля `totp_secret_enc/totp_enabled_at` остаются NULL для SSO-пользователей.
**Break-glass-аккаунт:**
- В schema добавлено `saas_admin_users.is_break_glass BOOLEAN DEFAULT FALSE`.
- При первой инициализации SaaS — создаётся **один** аккаунт `super_admin`с`sso_provider='local'`, `is_break_glass=TRUE`, обязательным TOTP, длинным random-паролем (хранится в 1Password DevOps vault, см. OPEN-И-12).
- Используется **только** при недоступности Yandex 360 IDP (>30 минут). Каждый вход break-glass — отдельный alert в Sentry severity=critical + email всем `super_admin` SaaS.
- Нельзя удалить или сделать `is_break_glass=FALSE` через UI — только прямой SQL под `crm_admin_user`.
4. `SaasAdminAuthService::handleSsoCallback($payload)`: validate JWT signature; lookup или JIT-create `saas_admin_users`; create `saas_admin_sessions`; redirect в `/admin`.
5. Логирование в `auth_log`с`actor_type='saas_admin'`, `event='sso_login_success'` (новое значение в whitelist).
**Configuration:** `config/services.php` секция `yandex360` — `client_id`, `client_secret` (из env), `redirect_uri`. Полный SAML/OIDC `xml`-проект Yandex 360 — отдельная задача DevOps в Прил. И.
### 22.13.2. CTO-13: Тест-план `SET LOCAL app.current_tenant_id` через PgBouncer
**Контекст:** все 35 RLS-политик (§22.6) полагаются на `current_setting('app.current_tenant_id')::bigint`. Это значение устанавливается через `SET LOCAL app.current_tenant_id = …` в начале каждого HTTP-request handler / job worker. **PgBouncer в transaction-pooling mode** возвращает соединение в пул после `COMMIT/ROLLBACK` каждой транзакции — `SET LOCAL` живёт ровно одну транзакцию.
**Риск без теста:** если разработчик случайно использует `SET` (session-scope) вместо `SET LOCAL`, или если `auto-commit=true` в Eloquent опускает первую транзакцию — `app.current_tenant_id` может протечь между тенантами. RLS станет декорацией.
**Тест-план (обязателен в спринте 1, до первого PR с tenant-моделью):**
1. **Auto-commit базовый кейс.** Установить `SET LOCAL app.current_tenant_id = 1`, выполнить `SELECT * FROM deals` (должно вернуть только tenant_id=1). Без `BEGIN`/`COMMIT` — что произойдёт? Ожидание: либо явный exception, либо корректное скоупирование. Молчаливая утечка → BLOCKER.
2. **Reuse соединения.** Сценарий: запрос tenant_id=1 завершается; PgBouncer возвращает соединение в пул; новый запрос tenant_id=2 берёт это же соединение. Проверить, что `app.current_tenant_id` НЕ виден из второго запроса (либо вообще undefined, либо явная очистка между транзакциями).
3. **Job retry.**`TenantAwareJob` сериализуется с`tenant_id`. При retry (после exception) — установка `SET LOCAL` корректна; нет утечки от предыдущего failed job в очереди.
4. **Долгая транзакция.**`BEGIN; SET LOCAL …; SELECT …; …; COMMIT;` 5-минутная транзакция. PgBouncer не должен «отнять» соединение для другого pool-юзера в середине.
5. **GUC parameter с `quoted` value.** Проверка корректного escaping `tenant_id` (который BIGINT, не должен иметь quotes — но всё равно проверить prepared statement).
**Критерий прохождения:** все 5 кейсов либо корректно изолируют tenant_id, либо явно падают с exception (приложение видит ошибку и не маскирует утечкой). **Молчаливая утечка = BLOCKER, тест не пройден, фаза 1 не открыта.**
**Альтернативы (не выбраны):** session-mode pooler — жертва производительности при ~100 соединений; `SET ROLE` per tenant — не масштабируется на тысячи ролей в `pg_roles`.
**Расширение Прил. И:** runbook «sprint 1 RLS smoke test» с конкретными SQL-командами и ожидаемыми результатами.
### 22.13.3. RLS WITH CHECK + REVOKE (OPEN-И-14)
См. подробности в schema CHANGELOG_schema.md §Y.6 + §Y.7.
**Кратко:**
- `WITH CHECK` добавлен к политикам `tenant_isolation` на `deal_tag_pivot` и `saas_invoice_items`. До v8.5 USING (фильтр SELECT/UPDATE) защищал чтение/обновление, но INSERT мог пройти при `tag_id`/`invoice_id` чужого тенанта.
- `REVOKE ALL ON saas_admin_users, saas_admin_sessions, saas_admin_audit_log, incidents_log, pd_subject_requests, impersonation_tokens FROM crm_app_user` — defense-in-depth. К saas-таблицам tenant-приложение доступа НЕ должно иметь даже теоретически (RLS + REVOKE = 2 барьера).
**Тест:** в фазе 1 RLS smoke-test (CTO-13) расширяется проверкой WITH CHECK — попытка INSERT в `deal_tag_pivot`с`tag_id` чужого тенанта должна падать с RLS exception.
### 22.13.4. Sentry PII-scrubbing (OPEN-И-16)
**Решение:** whitelist + regex маска для `phone/email/password/secret/token/api_key` во всех контекстах Sentry-event.
**Конфигурация в `config/sentry.php`:**
```php
'before_send' => function (\Sentry\Event $event, ?\Sentry\EventHint $hint) {
1. **Request data:** в `query`, `cookies`, `data`, `headers` — каждое значение проверяется regex'ом на pattern: `phone (^|[^a-z])(\+?7|8)\d{10}` → `+7XXXXXXXXXX`; `email \w+@\w+\.\w+` → `***@***.***`; ключи в whitelist `password|secret|token|api_key|webhook_token|totp_secret_enc` — value заменяется на `***`.
2. **Breadcrumbs:** для каждой crumb проходит то же scrub.
3. **Exception args:** для каждого frame в stacktrace — args редактируются.
4. **Whitelist полей:** в JSON-контекстах разрешены только `id`, `tenant_id`, `created_at`, `status`, `event`. Всё остальное по умолчанию заменяется на `[redacted]`. Это гарантирует, что новые поля (`inn`, `passport`, `dob`) автоматически не утекут в Sentry до явного добавления в whitelist.
**Тест:** unit-test `PiiScrubberTest` для каждого pattern + integration-test, что `Sentry::captureException()` после scrubber'а содержит только safe-значения.
**Юридический эффект:** Sentry self-hosted в Yandex Cloud (Ю-7), но даже там попадание ПДн = breach по 152-ФЗ ст.6 (обработка вне согласованных целей). Whitelist-подход страхует от регрессии при добавлении новых фич.
### 22.13.5. Anti-DDoS (OPEN-И-21)
**Стек:**
1. **Nginx `limit_req_zone`** глобально 1000 RPS на upstream Laravel. Per-IP лимиты остаются как в §22.3 (10 RPS логин, 60 RPS API).
2. **Yandex SmartCaptcha** на публичных эндпоинтах:
- `/register`
- `/login` после 2 неудач (CAPTCHA challenge)
- `/billing/topup` (cardholder verification)
3. **Disposable email blacklist** — список из ~50К доменов (`mailinator.com`, `tempmail.io`, etc.), обновляется раз в неделю. При регистрации с disposable email — отказ.
4. **Yandex Cloud Application Load Balancer DDoS-protection** — включён по умолчанию для нашего L7 ALB.
**Конфигурация:**
- Nginx: `nginx/conf.d/ratelimit.conf` (deployed через CI/CD).
- Disposable blacklist: ежедневный cron `accounts:refresh-disposable-list`, источник https://github.com/disposable-email-domains/disposable-email-domains.
**Бюджет:** Yandex SmartCaptcha — ~5 000 ₽/мес при ~50 000 challenges/мес. Учтено в смете.
### 22.13.6. TTL secrets (OPEN-И-17, OPEN-И-19)
**Изменения:**
- `api_keys.expires_at` теперь `NOT NULL DEFAULT NOW() + INTERVAL '365 days'` (v8.4 был NULL-able «бессрочный»). Миграция — backfill всем существующим NULL-ключам `NOW() + 365d` перед `SET NOT NULL`.
- `tenants.api_key_limit INT DEFAULT 5 NOT NULL CHECK (1..10)` — hard-limit количества активных ключей. Защита от DoS-через-создание-тысяч-ключей.
- Аналогичный TTL planned для `tenants.webhook_token` (rotation) и `outbound_webhook_subscriptions.secret_hash` (rotation) — реализация в §22.13.6.X cron.
**Cron `secrets:notify-expiring`** — раз в сутки в 09:00 МСК:
**Проблема:** до v8.5 `SaasAdminAuthService::createImpersonationToken()` создавал токен для любого тенанта без проверки compliance-флагов. Если у тенанта `pd_subject_request.processing_restricted=TRUE` (152-ФЗ ст.21 ч.5 — субъект ПДн запретил обработку) — admin SaaS мог несмотря на это войти в кабинет тенанта и просмотреть/изменить данные. Юридически это нарушение.
1. **Hard-block (Ю-9):** если у тенанта есть активный `pd_subject_request WHERE processing_restricted=TRUE` — токен можно создать **только** если `requestedBy.role = 'compliance'`. Иначе UI показывает заглушку: «Ограничение обработки ПДн (152-ФЗ ст.21). Доступ возможен только для роли compliance с письменным основанием».
2. **Two-person check (CTO-15):** дополнительно — если у тенанта `pd_subject_request.processing_restricted=TRUE` ИЛИ `chargeback_unrecovered_rub > 0`, то для подтверждения токена требуется второй админ роли `compliance`:
- При `createImpersonationToken()` — токен сохраняется с `second_approver_id=NULL`, `used_at=NULL`, и сразу блокируется (`invalidated_at IS NULL` пока не одобрено).
- На email второго `compliance`-админа — ссылка `/admin/imp/:token_id/approve` с описанием reason от первого админа.
- Второй `compliance` нажимает «Одобрить» → `second_approver_id` + `second_approval_at` заполняются → токен активируется (можно вводить 6-значный код от тенанта).
- Если второй `compliance` нажимает «Отклонить» → `invalidated_at = NOW()`, token deadlocked.
**Schema (v8.5):** `impersonation_tokens` + `second_approver_id BIGINT REFERENCES saas_admin_users(id)`, `second_approval_at TIMESTAMPTZ`. Если оба NULL — токен в обычном single-admin режиме (тенант без compliance-флагов).
**UX-требование:** в `/admin/tenants/:id` явный indicator «🔒 Two-person required» при наведении на «Войти как…» если флаги активны.
**Audit:** в `saas_admin_audit_log` для каждого создания/одобрения impersonation — отдельные записи action=`impersonation.token_created`, `impersonation.second_approval_granted`, `impersonation.second_approval_denied`.
См. подробности в §14.8.1 (full реализация). Здесь — security-перспектива.
**Threat model для audit-журнала:**
- **Insider:** недобросовестный admin SaaS пытается стереть свои действия. Защита: `audit_block_mutation()` triggers + REVOKE UPDATE/DELETE на роль `crm_audit_writer` + audit cron `audit:verify-chain`.
- **Database compromise:** если злоумышленник получает `crm_admin_user` (BYPASSRLS) credentials. Защита: hash chain — пересчёт цепочки выявит точку модификации. Восстановление — из бэкапов до момента compromise.
- **Insider with PG superuser:** вне нашей threat model (если суперпользователь PostgreSQL скомпрометирован — компрометирована вся база). Yandex Cloud Managed PostgreSQL не даёт нам superuser напрямую — это смягчает риск.
**Cron `audit:verify-chain`** — раз в сутки в 04:00 МСК (низкий бизнес-трафик). Отчёт в Prometheus + Grafana dashboard «Audit chain integrity». При detected break — alert в OnCall + создание `incidents_log type='audit_chain_break' severity='high'`.
> **Полная спецификация админки** — карта экранов, DDL новых таблиц (`saas_admin_users`, `saas_admin_audit_log`), дополнения к чек-листу заказчика, влияние на план спринтов — в [Прил. Г](Админка_SaaS_v8_2.md). Workflow ПДн — в [Прил. Д](Workflow_pd_subject_requests_v8_2.md).
> **Источник:** реестр v1.12 §13.10 (OPEN-И-13, CTO-15, Ю-9). Для углублённого описания см. также §22.13.1 (SSO flow), §22.13.7 (two-person + Ю-9).
**Изменения в админке SaaS v8.5:**
1. **Login screen `/admin/login` (OPEN-И-13).** Primary button — «Войти через Yandex 360» (OIDC). Secondary, маленький — «Аварийный вход» для break-glass-аккаунта. После успешного OIDC-callback — JIT-create `saas_admin_users` если новый пользователь, role default = `read_only`. Локальный 2FA выключен для SSO-юзеров (используется 2FA Yandex 360).
2. **Break-glass dashboard (OPEN-И-13).**В`/admin/system/break-glass` для `super_admin` — список текущих break-glass-входов (живые сессии + последние 30 дней). Каждый break-glass-вход формирует Sentry alert + email всем `super_admin`. Цель — раннее обнаружение злоупотребления break-glass (оно должно быть редким, при недоступности IDP).
3. **Two-person impersonation indicator (CTO-15).**В`/admin/tenants/:id` рядом с кнопкой «Войти как клиент»:
- Серый замок «🔒 Two-person required» — при наличии активного `pd_subject_request.processing_restricted=TRUE` ИЛИ `chargeback_unrecovered_rub > 0`. Hover показывает причину.
- При клике — модальное окно с двумя шагами: (а) основание (≥30 символов как раньше) + (б) выбор второго `compliance`-админа из списка.
- После Submit — токен сохраняется в pending-state (`second_approver_id=NULL`, `invalidated_at` пока не одобрено), email второму админу с deep-link на approve/decline.
4. **Approval queue для compliance-админов (CTO-15).** Новый экран `/admin/imp/pending-approvals` — список pending-токенов impersonation, ожидающих второго одобрения от текущего `compliance`-юзера. Можно одобрить (заполнить `second_approver_id`/`second_approval_at`) или отклонить (`invalidated_at = NOW()`). Все действия → `saas_admin_audit_log`.
5. **152-ФЗ ст.21 hard-block UI (Ю-9).** Если `pd_subject_request.processing_restricted=TRUE` И `requestedBy.role != 'compliance'`:
- Кнопка «Войти как клиент» **полностью скрыта** (не disabled, а скрыта).
- Вместо неё — серый блок: «🔒 Доступ ограничен по 152-ФЗ ст.21. Только роль `compliance` с письменным основанием».
- Если admin всё-таки попадёт через прямой URL `/admin/tenants/:id/imp/create` (например, кэшированная ссылка) — `SaasAdminAuthService` вернёт 403 + audit-запись `event='imp_blocked_152fz_st21'`.
**Cross-references:**
- Полный flow OIDC + конфигурация — Прил. И (Runbook) v8.5 «Yandex 360 SSO setup».
- Schema-изменения — `saas_admin_users.sso_provider/is_break_glass`, `impersonation_tokens.second_approver_id/second_approval_at` (см. CHANGELOG_schema.md §Y.2.1, §Y.2.2).
- Threat model two-person — §22.13.7.
---
# 24. Тестирование
@@ -5951,4 +6481,4 @@ Drag-and-drop для Kanban — `vue-draggable-plus` или `@formkit/drag-and-d
---
*Конец документа CRM_bp-gr_Инструкция_v8_4.md (06.05.2026, финал v8.4).*
*Конец документа CRM_bp-gr_Инструкция_v8_5.md (07.05.2026, v8.5 — реализация 27 решений аудита C). Финал v8.4 был 06.05.2026.*
**Состав:** 17 файлов в `docs/` + 2 в `db/` + `CLAUDE.md` + `README.md` в корне репозитория (1 главный narrative v8.4 + 12 шифров приложений [А, Б, В, Г, Д, Е, Ж, З, И, К, М, **Н**] в 11 файлах [Б+В физически в одном] + brandbook + 3 служебных + 1 правила работы Claude + 1 реестр tooling). *См. историю изменений ниже: v8.4 = финал narrative v8.4 (все 13 разделов плана переписаны), переименование `_v8_3.md` → `_v8_4.md`, schema.sql v8.4 (53 табл/86 инд/33 RLS, +outbound_webhook_*), удаление промежуточного `Plan_narrative_v8_4.md`. Архитектурных изменений после v8.3 + outbound webhook: уже зафиксировано в schema.*
**Состав:** 17 файлов в `docs/` + 2 в `db/` + `CLAUDE.md` + `README.md` в корне репозитория (1 главный narrative v8.5 + 12 шифров приложений [А, Б, В, Г, Д, Е, Ж, З, И, К, М, **Н**] в 11 файлах [Б+В физически в одном] + brandbook + 3 служебных + 1 правила работы Claude + 1 реестр tooling). *См. историю изменений ниже: v8.5 = реализация 27 решений аудита C из реестра v1.12 (07.05.2026). Schema.sql v8.5 (54 табл/91 инд/35 RLS/4 роли/12 триггеров/4 функции; +`project_user_assignments`, +`crm_audit_writer`, +6 audit-триггеров append-only + hash chain). Narrativev8.5 — переписаны §7.1 (метрики), добавлены §10.8 (антифрод+routing+scoring+region+escalation), §12.5.5 (TTFR+UTM cohort), §14.8 (audit hash chain + export log), §17.9 (Telegram + late payment alerts), §19.10.11–12 (DNS-rebinding + marketing.conversion), §22.13 (SSO/Sentry/SET LOCAL/anti-DDoS/TTL secrets/two-person), §23.10.11 (break-glass + two-person UI), Прил.И Часть Г (9 операционных процедур).*
**Эволюция версий:**
v8.0 (25.04.2026)
@@ -14,8 +14,9 @@ v8.0 (25.04.2026)
→ v8.3++ optimized + правила Claude (05.05.2026, поздний вечер): `Konspekt_sessii_05_05_2026.md` удалён (информация в `Объединённый_конспект.md`); добавлены `Объединённый_конспект.md` и `Pravila_raboty_Claude_v1_1.md`; 16 → 17 файлов.
→ v8.3.2 (05.05.2026, поздний вечер, итерация 2): закрытие 3 🟡-расхождений бэклога v8.4 + закрытие Биз-10 переоткрытого + синхронизация narrative до v8.3.1 + устранение унаследованного расхождения сводки §0 в Прил. Е.
→ **v8.3.3 (06.05.2026): добавление Прил. Н — реестра 28 инструментов разработки, скиллов Claude Code, MCP-серверов и плагинов в 4 фазах (фаза 0 — текущая, +1, +2, +3). Создан корневой `CLAUDE.md` — оперативная карта для Claude Code (приоритет правил, стек, карта инструментов, запреты). Шифр Н занят. Pravila_raboty_Claude обновлены до v1.2 (§4.8 — добавление Н в занятые шифры). Открытые_вопросы обновлены до v1.9 (упоминание Прил. Н в шапке). Архитектурных изменений: 0. Правки в 3 файлах. (1) `schema.sql` — добавлены явные комментарии «-- НЕ tenant-уровневая. RLS не применяется намеренно» к `impersonation_tokens` и `pd_subject_requests` (false-positive в self-review v8.3.1). (2) `Открытые_вопросы` v1.7 → v1.8: 📜-пометка к историческому хвосту v1.5→v1.6, Биз-10 ⏸ → ✅, устранено унаследованное расхождение сводки §0 (шапка ✅ 30 при сумме строк 32 в v1.7 → шапка ✅ 33 при сумме строк 33 в v1.8; теперь арифметически согласовано). (3) `CRM_bp-gr_Инструкция_v8_4.md` v8.3 → v8.3.1 — синхронизация 8 точек по Биз-10 (§6.3, §9, §10.2, §10.3, §12.2, §17.5, §17.6): удалены устаревшие `deals.reminder_text`/`deals.reminder_at`/`is_done`/`idx_deals_reminder`, переписан DDL `reminders` по schema.sql v8.3 (`created_by`, `assignee_id`, `completed_at`, RLS), добавлен паритетный фильтр `?reminders=today\|last\|future\|none`. **Архитектурных изменений: 0.** Состав: 17 файлов (без изменений).**
→ **v8.4 (06.05.2026, поздний вечер): финал narrative v8.4.** Все 13 разделов плана переписаны (§1, §5, §7, §8, §9, §12, §14, §17, §18.4, §19.10, §22, §23.10, §26). Главный narrative переименован: `CRM_bp-gr_Инструкция_v8_4.md` → `CRM_bp-gr_Инструкция_v8_4.md`. Schema → v8.4 (+`outbound_webhook_subscriptions`, +`outbound_webhook_deliveries`, +2 RLS, +5 индексов). Метрики schema: 51/81/31 → **53/86/33**. Открытые_вопросы → v1.10 (закрыто 11 вопросов с дефолтами; все P2 закрыты; 50 продуктовых: 40✅/5🟦/5⏸; 0 P2). Промежуточный `Plan_narrative_v8_4.md` удалён (план выполнен). Прил. Н, Прил. М, brandbook — без изменений. Состав: 17 файлов в `docs/` (минус удалённый Plan) + 2 в `db/` + CLAUDE.md + README.md в корне.
→ **v8.3.3 (06.05.2026): добавление Прил. Н — реестра 28 инструментов разработки, скиллов Claude Code, MCP-серверов и плагинов в 4 фазах (фаза 0 — текущая, +1, +2, +3). Создан корневой `CLAUDE.md` — оперативная карта для Claude Code (приоритет правил, стек, карта инструментов, запреты). Шифр Н занят. Pravila_raboty_Claude обновлены до v1.2 (§4.8 — добавление Н в занятые шифры). Открытые_вопросы обновлены до v1.9 (упоминание Прил. Н в шапке). Архитектурных изменений: 0. Правки в 3 файлах. (1) `schema.sql` — добавлены явные комментарии «-- НЕ tenant-уровневая. RLS не применяется намеренно» к `impersonation_tokens` и `pd_subject_requests` (false-positive в self-review v8.3.1). (2) `Открытые_вопросы` v1.7 → v1.8: 📜-пометка к историческому хвосту v1.5→v1.6, Биз-10 ⏸ → ✅, устранено унаследованное расхождение сводки §0 (шапка ✅ 30 при сумме строк 32 в v1.7 → шапка ✅ 33 при сумме строк 33 в v1.8; теперь арифметически согласовано). (3) `CRM_bp-gr_Инструкция_v8_5.md` v8.3 → v8.3.1 — синхронизация 8 точек по Биз-10 (§6.3, §9, §10.2, §10.3, §12.2, §17.5, §17.6): удалены устаревшие `deals.reminder_text`/`deals.reminder_at`/`is_done`/`idx_deals_reminder`, переписан DDL `reminders` по schema.sql v8.3 (`created_by`, `assignee_id`, `completed_at`, RLS), добавлен паритетный фильтр `?reminders=today\|last\|future\|none`. **Архитектурных изменений: 0.** Состав: 17 файлов (без изменений).**
→ **v8.4 (06.05.2026, поздний вечер): финал narrative v8.4.** Все 13 разделов плана переписаны (§1, §5, §7, §8, §9, §12, §14, §17, §18.4, §19.10, §22, §23.10, §26). Главный narrative переименован: `CRM_bp-gr_Инструкция_v8_3.md` → `CRM_bp-gr_Инструкция_v8_5.md`. Schema → v8.4 (+`outbound_webhook_subscriptions`, +`outbound_webhook_deliveries`, +2 RLS, +5 индексов). Метрики schema: 51/81/31 → **53/86/33**. Открытые_вопросы → v1.10 (закрыто 11 вопросов с дефолтами; все P2 закрыты; 50 продуктовых: 40✅/5🟦/5⏸; 0 P2). Промежуточный `Plan_narrative_v8_4.md` удалён (план выполнен). Прил. Н, Прил. М, brandbook — без изменений. Состав: 17 файлов в `docs/` (минус удалённый Plan) + 2 в `db/` + CLAUDE.md + README.md в корне.
→ **v8.5 (07.05.2026): реализация 27 решений аудита C из реестра v1.12.** schema.sql v8.4 → v8.5 (53→54 табл, 86→91 инд, 33→34 RLS, 34→35 ENABLE RLS, 3→4 роли, 0→12 триггеров, 0→4 функции, +`project_user_assignments`, +`crm_audit_writer`, ~26 новых колонок, ALTER `api_keys.expires_at SET NOT NULL DEFAULT NOW()+365d`). Narrative v8.4 → v8.5 (переименован файл; добавлены §10.8/§12.5.5/§14.8/§17.9/§19.10.11–12/§22.13/§23.10.11; обновлены метрики §7.1). README архива v8.4 → v8.5. Прил. И → v0.3 (+Часть Г: 9 процедур). 8 P0 разблокировали триггер фазы 1. Открытые_вопросы → v1.12 (67✅/5🟦/5⏸ из 77; 0 P2). Состав: **17 файлов в `docs/`** + 2 в `db/` + CLAUDE.md + README.md (без изменений в составе — переименования не меняют count).
**Рабочее название продукта:** **Лидпоток**.
@@ -41,7 +42,7 @@ v8.0 (25.04.2026)
- Прил. Д (Workflow ПДн), Прил. Ж (Оферта/Политика), Прил. З (Уведомление РКН) — отдельные юр. документы для разных юристов / РКН.
- Прил. М (Анализ оригинала) — самостоятельный документ-обоснование.
**Что в принципе не существует, но фигурирует в кросс-ссылках архива:** конспект интервью 04.05.2026 (`Konspekt_sessii_04_05_2026_intervyu.md`) и единый отчёт аудита 04.05.2026 (`crm-bp-gr-audit-2026-05-04.md`) **в этом архиве не приложены** — они хранятся в предыдущей итерации. Решения 04.05 интегрированы в шапку `CRM_bp-gr_Инструкция_v8_4.md`, выводы аудита 04.05 — в `Прил_М_Analiz_originala_v8_3.md` v1.1.
**Что в принципе не существует, но фигурирует в кросс-ссылках архива:** конспект интервью 04.05.2026 (`Konspekt_sessii_04_05_2026_intervyu.md`) и единый отчёт аудита 04.05.2026 (`crm-bp-gr-audit-2026-05-04.md`) **в этом архиве не приложены** — они хранятся в предыдущей итерации. Решения 04.05 интегрированы в шапку `CRM_bp-gr_Инструкция_v8_5.md`, выводы аудита 04.05 — в `Прил_М_Analiz_originala_v8_3.md` v1.1.
### Приложения (12 шифров — А, Б, В, Г, Д, Е, Ж, З, И, К, М, Н) — 11 файлов
@@ -125,7 +126,7 @@ v8.0 (25.04.2026)
### Зафиксировано 30 продуктовых решений
Подробно — в `Объединённый_конспект.md` (Часть V «v8.3+ → v8.3++ optimized», 05.05.2026), и (для интервью 04.05) в шапке `CRM_bp-gr_Инструкция_v8_4.md`.
Подробно — в `Объединённый_конспект.md` (Часть V «v8.3+ → v8.3++ optimized», 05.05.2026), и (для интервью 04.05) в шапке `CRM_bp-gr_Инструкция_v8_5.md`.
Сводка:
@@ -250,13 +251,13 @@ v8.0 (25.04.2026)
### Заказчик / архитектор проекта
1. `CRM_bp-gr_Инструкция_v8_4.md` — раздел «Что нового в v8.3» в шапке.
1. `CRM_bp-gr_Инструкция_v8_5.md` — раздел «Что нового в v8.3» в шапке.
2. `Analiz_originala_v8_3.md` (Прил. М) v1.1 — разделы 0, 2, 5 (TL;DR + главные открытия + 7 новых вопросов с рекомендациями).
3. `Открытые_вопросы_v8_3.md` v1.10 — раздел 10 «Что осталось от заказчика» + разделы 11–12 (Биз-10..16).
### Backend-разработчик
1. `CRM_bp-gr_Инструкция_v8_4.md` — раздел «Что нового в v8.3» (обязательно), потом разделы 1–7, 20–22.
1. `CRM_bp-gr_Инструкция_v8_5.md` — раздел «Что нового в v8.3» (обязательно), потом разделы 1–7, 20–22.
2. **`Analiz_originala_v8_3.md` (Прил. М) v1.1 — обязательно**: §2 (новые архитектурные сущности), §3 (DDL для schema.sql v8.2 и v8.3), §3.5 (детальный DDL партий 12–15), §4 (изменения в narrative), §9 (детальная развёртка партий 12–15 для backend).
3. **Приложение А (`schema.sql` v8.3)** — единая консолидированная схема.
4. **`CHANGELOG_schema.md`** — спутник: запись A (v8.2 → v8.3), запись B (v8.1 → v8.2). Eloquent-модели, сервисы, миграция, тестирование.
3. **`Analiz_originala_v8_3.md` (Прил. М) v1.1** §4.5 — спецификация карточки проекта (паритет с оригиналом + наши расширения), §4.4 — карточка сделки всегда editable, §9.4.4 — антипаттерны оригинала с credentials (использовать `type="password"`), §9.3 — формат «Тихих часов».
4. Приложение Г v8.2 — спецификации UI админки SaaS.
### DevOps
1. `CRM_bp-gr_Инструкция_v8_4.md` — раздел «Что нового в v8.3» (DO-1, DO-3, DO-5, OPEN-К-2..8).
1. `CRM_bp-gr_Инструкция_v8_5.md` — раздел «Что нового в v8.3» (DO-1, DO-3, DO-5, OPEN-К-2..8).
2. **Приложение К v1.1** — обоснование выбора Yandex Cloud + список патчей в v8.4.
3. **`Analiz_originala_v8_3.md` (Прил. М) v1.1 §6, §6.6** — обнаруженная prompt injection-атака в DOM оригинала + новые антипаттерны из партий 12–15 (пароль plaintext, API-ключи в text-input); CSP-требования для нашей платформы (§4.9).
4. Приложение З v8.2 — пункт 9 (адрес ЦОД), пункт 11 (УЗ-4).
@@ -291,7 +292,7 @@ v8.0 (25.04.2026)
### Бухгалтер
1. `Открытые_вопросы_v8_3.md` v1.10 раздел 2 — Б-1 (ждём от вас) и решения по Б-2..6.
2. `CRM_bp-gr_Инструкция_v8_4.md` шапка v8.3 раздел «Биллинг» — формат XLSX/email, минимум 100₽, округление вниз, 1С 8.3 XML.
2. `CRM_bp-gr_Инструкция_v8_5.md` шапка v8.3 раздел «Биллинг» — формат XLSX/email, минимум 100₽, округление вниз, 1С 8.3 XML.
| **v8.3++ optimized** | **05.05.2026 (вечер)** | **Структурная оптимизация архива (Вариант B + SVG inline): объединено 11 файлов в 5. CHANGELOG-v8_2 + v8_3 → CHANGELOG_schema.md. Прил. Б + Прил. В → Приложение_Б_В_БД_диаграммы_v8_3.md. 4 файла аудита 12–15 → Аудит_partii_12_15_originala_v8_3.md. 2 конспекта 05.05 → Konspekt_sessii_05_05_2026.md. 5 SVG → inline в brandbook.md v1.1 §9.1–9.5. **Информация полностью сохранена** — только структурное укрупнение. Итог: 21 → 16 файлов (-24%).** |
| **v8.3++ optimized + правила Claude** | **05.05.2026 (поздний вечер)** | **Удалён `Konspekt_sessii_05_05_2026.md` (информация дублируется в `Объединённый_конспект.md`, потери нет). Добавлены: `Объединённый_конспект.md` (свод по 6 исходным конспектам сессий), `Pravila_raboty_Claude_v1_1.md` (свод правил работы Claude в проекте, утверждён заказчиком, скопирован в Project instructions). Итог: 16 → 17 файлов.** |
| **v8.3.1** | **05.05.2026 (поздний вечер)** | **Аудит связности архива (отчёт в чате 05.05) + точечные правки. (1) Разрешена коллизия `OPEN-И-2`: «контакты эскалации» переименованы в `OPEN-И-12` (P1, вариант B′ по решению заказчика); смыслы `OPEN-И-2` (CRM-интеграции, ✅) и `OPEN-И-11` (миграции `crm_connections`, ✅) сохранены. (2) Синхронизирована таблица OPEN-И-* в `Runbook_ekspluatatsii_v8_2.md` Часть В с шапкой (была расхожесть ✅/⏸ для OPEN-И-1/2). (3) Исправлены 4 ссылки на удалённый `Konspekt_sessii_05_05_2026.md` → `Объединённый_конспект.md` (Части V–VI). (4) Добавлены пометки 📜 ИСТОРИЧЕСКАЯ ЗАПИСЬ к таблице приложений §28 narrative. (5) `Открытые_вопросы` → v1.7. **Затронуто файлов: 5** (`Runbook_ekspluatatsii_v8_2.md`, `Объединённый_конспект.md`, `Открытые_вопросы_v8_3.md`, `CRM_bp-gr_Инструкция_v8_4.md`, `Аудит_partii_12_15_originala_v8_3.md` + этот README). **Архитектурных изменений: 0.** Итог: 17 файлов (без изменений в составе).** |
| **v8.3.1** | **05.05.2026 (поздний вечер)** | **Аудит связности архива (отчёт в чате 05.05) + точечные правки. (1) Разрешена коллизия `OPEN-И-2`: «контакты эскалации» переименованы в `OPEN-И-12` (P1, вариант B′ по решению заказчика); смыслы `OPEN-И-2` (CRM-интеграции, ✅) и `OPEN-И-11` (миграции `crm_connections`, ✅) сохранены. (2) Синхронизирована таблица OPEN-И-* в `Runbook_ekspluatatsii_v8_2.md` Часть В с шапкой (была расхожесть ✅/⏸ для OPEN-И-1/2). (3) Исправлены 4 ссылки на удалённый `Konspekt_sessii_05_05_2026.md` → `Объединённый_конспект.md` (Части V–VI). (4) Добавлены пометки 📜 ИСТОРИЧЕСКАЯ ЗАПИСЬ к таблице приложений §28 narrative. (5) `Открытые_вопросы` → v1.7. **Затронуто файлов: 5** (`Runbook_ekspluatatsii_v8_2.md`, `Объединённый_конспект.md`, `Открытые_вопросы_v8_3.md`, `CRM_bp-gr_Инструкция_v8_5.md`, `Аудит_partii_12_15_originala_v8_3.md` + этот README). **Архитектурных изменений: 0.** Итог: 17 файлов (без изменений в составе).** |
| **v8.3.3** | **06.05.2026** | **Добавление Прил. Н — реестра 28 инструментов разработки, скиллов Claude Code, MCP-серверов и плагинов в 4 фазах. Создан корневой `CLAUDE.md` — оперативная карта для Claude Code (приоритет 5 уровней правил, стек проекта, карта 28 инструментов «когда что использовать», 10 запретов). Обновлены: `Pravila_raboty_Claude_v1_1.md` v1.1 → v1.2 (§4.8 — Н в занятые шифры; счётчик занятых: 11 → 12), `Открытые_вопросы_v8_3.md` v1.8 → v1.9 (упоминание Прил. Н в шапке; продуктовых вопросов не добавлено). Обновлён корневой `README.md` — раздел «Документация для разработчика» со ссылками на `CLAUDE.md` и Прил. Н. **Затронуто файлов:** 6 (`docs/Tooling_v8_3.md` — новый, `CLAUDE.md` — новый, `docs/README_АРХИВ_v8_3.md`, `docs/Pravila_raboty_Claude_v1_1.md`, `docs/Открытые_вопросы_v8_3.md`, `README.md` корня + этот же README архива). **Архитектурных изменений: 0.****Состав: 18 файлов** в `docs/`+`db/` + `CLAUDE.md` в корне (было: 17 в `docs/`+`db/`). |
| **v8.4** | **06.05.2026** (поздний вечер) | **Финал narrative v8.4 + финализация архива.** В одну сессию: (1) переписаны все 13 разделов плана v8.4 (§1, §5, §7, §8, §9, §12, §14, §17, §18.4, §19.10, §22, §23.10, §26 — 6 коммитов от `938066f` до `c8db9a2`); (2) schema.sql v8.3 → v8.4: +`outbound_webhook_subscriptions`, +`outbound_webhook_deliveries`, +2 RLS, +5 индексов, метрики 51/81/31 → **53/86/33** (закрытие тех-долга §19.10, OPEN-И-2 Уровень 1); (3) Открытые_вопросы v1.9 → v1.10: закрыто 11 вопросов с дефолтами, все P2 закрыты, 50 продуктовых: 40✅/5🟦/5⏸; (4) main narrative переименован: `CRM_bp-gr_Инструкция_v8_3.md` → `_v8_4.md` (через `git mv`); (5) промежуточный `Plan_narrative_v8_4.md` удалён (план выполнен 13/13); (6) обновлены кросс-ссылки в `CLAUDE.md` (§0/§2/§6/§8 — версии, метрики 53/86/33, прототипы 3/8), `README.md` (статусы прототипов, репозиторий CoralMinister), `db/schema.sql`, `db/CHANGELOG_schema.md` (запись §Z), `web/index.html`, `.lychee.toml` (exclude приватного репозитория); (7) этот README архива переименован в `README_АРХИВ_v8_4.md`. **Затронуто файлов:** 8 (narrative переименован + 7 правок). **Архитектурных изменений в v8.4 относительно v8.3:** outbound webhook (только) — 2 таблицы, изменение статуса с «Post-MVP» на «MVP» по итогам реверс-инжиниринга оригинала (партия 15). **Состав: 17 файлов** в `docs/` (минус удалённый Plan) + 2 в `db/` + CLAUDE.md + README.md в корне. |
| v8.3.2 | 05.05.2026 (поздний вечер, итерация 2) | Закрытие бэклога 🟡-расхождений + закрытие Биз-10 переоткрытого + синхронизация narrative + устранение унаследованной ошибки счётчика сводки. (1) `schema.sql` — добавлены явные комментарии «-- НЕ tenant-уровневая. RLS не применяется намеренно: …» к `impersonation_tokens` и `pd_subject_requests` (метрики не изменились: 51 таблица + 12 партиций, 81 индекс, 31 RLS, 0 orphan-FK). (2) `Открытые_вопросы` v1.7 → v1.8: 📜-пометка к историческому хвосту v1.5→v1.6 (стр. 432); закрыт **Биз-10** окончательно (⏸ переоткрытый → ✅), решение — минимальный паритет с оригиналом (отдельная таблица `reminders` без приоритетов/каналов/recurrence); устранено унаследованное расхождение сводки §0 (шапка `✅ 30` при сумме строк 32 в v1.7 → теперь арифметически согласовано: `✅ 33`, `⏸ 12`, `P2 7`, добавлена сноска про принцип «арифметическая сумма строк»). (3) `CRM_bp-gr_Инструкция_v8_4.md` v8.3 → v8.3.1 — синхронизация **8 точек** по Биз-10: §6.3 (импорт CSV: `deals.reminder_at` → `INSERT INTO reminders`), §9 (DDL `deals` без `reminder_text`/`reminder_at`/`idx_deals_reminder`), §10.2 (колонка «Ближайшее активное напоминание» с подзапросом), §10.3 (`EXISTS`-подзапрос + паритетный фильтр `?reminders=today\|last\|future\|none`), §12.2 (KPI: `is_done = false` → `completed_at IS NULL`), §17.5 (DDL `reminders` переписан: `created_by`, `assignee_id`, `completed_at`, 4 индекса, RLS), §17.6 (cron: `where('is_done', false)` → `whereNull('completed_at')`). **Затронуто файлов: 4** (`schema.sql`, `Открытые_вопросы_v8_3.md`, `CRM_bp-gr_Инструкция_v8_4.md`, `Объединённый_конспект.md` Часть VIII + этот README). **Архитектурных изменений: 0.** Итог: 17 файлов (без изменений в составе).** |
| v8.3.2 | 05.05.2026 (поздний вечер, итерация 2) | Закрытие бэклога 🟡-расхождений + закрытие Биз-10 переоткрытого + синхронизация narrative + устранение унаследованной ошибки счётчика сводки. (1) `schema.sql` — добавлены явные комментарии «-- НЕ tenant-уровневая. RLS не применяется намеренно: …» к `impersonation_tokens` и `pd_subject_requests` (метрики не изменились: 51 таблица + 12 партиций, 81 индекс, 31 RLS, 0 orphan-FK). (2) `Открытые_вопросы` v1.7 → v1.8: 📜-пометка к историческому хвосту v1.5→v1.6 (стр. 432); закрыт **Биз-10** окончательно (⏸ переоткрытый → ✅), решение — минимальный паритет с оригиналом (отдельная таблица `reminders` без приоритетов/каналов/recurrence); устранено унаследованное расхождение сводки §0 (шапка `✅ 30` при сумме строк 32 в v1.7 → теперь арифметически согласовано: `✅ 33`, `⏸ 12`, `P2 7`, добавлена сноска про принцип «арифметическая сумма строк»). (3) `CRM_bp-gr_Инструкция_v8_5.md` v8.3 → v8.3.1 — синхронизация **8 точек** по Биз-10: §6.3 (импорт CSV: `deals.reminder_at` → `INSERT INTO reminders`), §9 (DDL `deals` без `reminder_text`/`reminder_at`/`idx_deals_reminder`), §10.2 (колонка «Ближайшее активное напоминание» с подзапросом), §10.3 (`EXISTS`-подзапрос + паритетный фильтр `?reminders=today\|last\|future\|none`), §12.2 (KPI: `is_done = false` → `completed_at IS NULL`), §17.5 (DDL `reminders` переписан: `created_by`, `assignee_id`, `completed_at`, 4 индекса, RLS), §17.6 (cron: `where('is_done', false)` → `whereNull('completed_at')`). **Затронуто файлов: 4** (`schema.sql`, `Открытые_вопросы_v8_3.md`, `CRM_bp-gr_Инструкция_v8_5.md`, `Объединённый_конспект.md` Часть VIII + этот README). **Архитектурных изменений: 0.** Итог: 17 файлов (без изменений в составе).** |
| v0.1 | 04.05.2026 | Первый Runbook в рамках сессии 03–04.05.2026, на основе архитектуры v8.1. Покрывает 9 типовых инцидентов |
| v0.2 (план) | После закрытия OPEN-И-1, И-2, И-5 | Структурные таблицы, шаблоны, контакты. Ввод в реальную работу с момента запуска staging |
| **v0.3** | **07.05.2026** | **Дополнения по реализации 27 решений аудита C (v1.12). Новые процедуры — см. Часть Г ниже** |
| v1.0 (план) | После 6 месяцев эксплуатации | Журнал прецедентов с реальными инцидентами; пересмотр процедур по итогам |
---
*Драфт v0.1 от 04.05.2026. Готовится в рамках сессии 03–04.05.2026 как Приложение И к v8.1 (закрытие 🟡 №11 — runbook эксплуатации).*
# Часть Г. v8.5 — процедуры по аудиту C (07.05.2026)
> **Источник:** реестр Открытые_вопросы_v8_3.md v1.12 §13.10. Реализация — schema.sql v8.5 (коммит `038a884`) + narrative v8.5 §22.13. Все процедуры ниже становятся частью обязательного operational baseline с момента деплоя v8.5 в staging.
## Г.1. CTO-13: RLS smoke-test через PgBouncer (обязательно в спринте 1)
**Когда:** перед первым PR, который использует `TenantAwareJob` или `SET LOCAL app.current_tenant_id`. Без прохождения теста — **триггер фазы 1 не открывается**.
**Что проверить:**
```sql
-- Кейс 1: auto-commit базовый
SET LOCAL app.current_tenant_id = 1;
SELECT COUNT(*) FROM deals WHERE tenant_id = 1; -- ожидание: успех
SELECT COUNT(*) FROM deals WHERE tenant_id = 2; -- ожидание: 0 (RLS блокирует)
-- Кейс 2: reuse соединения через PgBouncer (transaction-pooling)
-- В session 1: SET LOCAL → SELECT → COMMIT.
-- В session 2 (то же физ. соединение): SELECT без SET LOCAL.
-- Ожидание: либо exception (current_setting не установлен),
-- либо value сброшен (NULL/empty).
-- Молчаливое наследование значения от session 1 → BLOCKER.
-- Кейс 3: job retry
-- TenantAwareJob с tenant_id=1 падает с exception → retry.
-- При retry: SET LOCAL = 1 в начале handle().
-- Ожидание: tenant_id корректно установлен из payload, не из памяти worker'а.
-- Кейс 4: WITH CHECK защита (OPEN-И-14)
SET LOCAL app.current_tenant_id = 1;
INSERT INTO deal_tag_pivot (deal_id, tag_id) VALUES (1, <tag_id чужого тенанта>);
-- Ожидание: RLS exception на WITH CHECK.
-- Кейс 5: REVOKE на saas-таблицах (OPEN-И-14)
SET ROLE crm_app_user;
SELECT * FROM saas_admin_users LIMIT 1;
-- Ожидание: permission denied.
```
**Результат:** все 5 кейсов либо корректно изолируют tenant_id, либо явно падают с exception. Формальный отчёт — `docs/sprint1_rls_smoke.md` (создаётся в спринте 1) с актуальными PG/PgBouncer версиями.
## Г.2. OPEN-И-15: Cron `audit:verify-chain` (раз в сутки)
3. **Не блокируем работу системы** — audit-таблицы продолжают писаться. Расследование — отдельный процесс с привлечением CTO.
## Г.3. OPEN-И-17: Cron `secrets:notify-expiring` (раз в сутки)
**Расписание:** `09:00 МСК` ежедневно.
**Логика:** SELECT api_keys WHERE `expires_at <= NOW() + INTERVAL '30 days' AND is_active=TRUE`. GROUP BY (`tenant_id`, `user_id`). Один email на группу в день. Subject: «Истекают API-ключи: {N} шт. через {3-30} дней». Тело — список с `key_prefix`, `expires_at`, deep-link на «Продлить».
**Действие пользователя:** клик → `/api-keys` → у каждого ключа кнопка «Продлить на год» (`expires_at = NOW() + INTERVAL '365 days'`). Аудит в `activity_log` event=`api_key.extended`.
**При истечении (`expires_at <= NOW()`):** ключ автоматически переходит в `is_active=FALSE` через cron `api_keys:deactivate-expired` (раз в час). Запросы с этим ключом → 401.
**Backend:** middleware `App\Http\Middleware\VerifyYandexCaptcha` проверяет `captcha_token` через POST к `https://captcha-api.yandex.ru/validate`. При неудаче → 429.
**Бюджет:** ~5 000 ₽/мес при 50К challenges (стандартный pricing 2026).
### Г.4.3. Disposable email blacklist
**Cron `accounts:refresh-disposable-list`** — раз в неделю (среда, 03:00 МСК):
5. Создать первый break-glass: `super_admin` с `sso_provider='local'`, `is_break_glass=TRUE`. Длинный random-пароль в 1Password DevOps vault. TOTP enrolled.
7. `incidents_log` запись `event='yandex_sso_enabled'`.
**Откат:** при неработоспособности Yandex 360 — break-glass-аккаунт + временное переключение `is_break_glass=TRUE` для всех `super_admin` через прямой SQL под `crm_admin_user`. После восстановления IDP — обратно `FALSE`.
2. Reassign: если `assignment_strategy='manual'` — supervisor проекта; если `round_robin`/`least_loaded` — следующий active member из `project_user_assignments`.
3. Email (см. §17.9.2 narrative) — старому + новому менеджеру + админу тенанта при `escalated_count >= 2`.
4. `activity_log` event=`deal.escalated`.
**Индекс:** `(tenant_id, assigned_at) WHERE status NOT IN ('closed','rejected')` — для эффективного scan.
## Г.9. Биз-24: Cron `payments:notify-stale`
**Расписание:** раз в час (`0 ****`).
**Логика:** SELECT saas_invoices WHERE `status='waiting_payment' AND created_at + INTERVAL '48 hours' < NOW() AND notified_finance=FALSE`. Для каждого: email finance-роли + bell-нотификация в `/admin/billing/stale-invoices` + `notified_finance = TRUE`.
**Дополнительно** (default off): через 7 дней повторный алерт + создание `incidents_log` type='payment_overdue'.
Прил. Л v0.1 · Источник истины по визуалу: <code>brandbook.md</code> v1.1 ·
По поведению: <code>CRM_bp-gr_Инструкция_v8_4.md</code> §1–28 + Прил. Г для №08
По поведению: <code>CRM_bp-gr_Инструкция_v8_5.md</code> §1–28 + Прил. Г для №08
</p>
</div>
</body>
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.