diff --git a/CLAUDE.md b/CLAUDE.md index bcc448f5..2a8e49ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,7 @@ |---|---| | Продуктовые правила работы Claude | [docs/Pravila_raboty_Claude_v1_1.md](docs/Pravila_raboty_Claude_v1_1.md) (v1.2+) | | Полный реестр 28 инструментов и фазы | [docs/Tooling_v8_3.md](docs/Tooling_v8_3.md) (Прил. Н v1.0+) | -| Главное ТЗ | [docs/CRM_bp-gr_Инструкция_v8_4.md](docs/CRM_bp-gr_Инструкция_v8_4.md) (v8.4 от 06.05.2026) | +| Главное ТЗ | [docs/CRM_bp-gr_Инструкция_v8_5.md](docs/CRM_bp-gr_Инструкция_v8_5.md) (v8.5 от 07.05.2026 — реализация 27 решений аудита C) | | Схема БД | [db/schema.sql](db/schema.sql) (v8.5 от 07.05.2026 — реализация 27 решений аудита C, narrative v8.5 готовится) | | Открытые вопросы | [docs/Открытые_вопросы_v8_3.md](docs/Открытые_вопросы_v8_3.md) (v1.12+) | | Брендбук | [docs/brandbook.md](docs/brandbook.md) (v1.1) | diff --git a/README.md b/README.md index 009ecdb8..955bde03 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ lidpotok/ |---|---| | Палитра, типографика, размерная сетка | `brandbook.md` v1.1 §3, §4, §5, §8 | | SVG-логотипы | `brandbook.md` §9.1–9.5 | -| Поведение экранов | `CRM_bp-gr_Инструкция_v8_4.md` v8.4 | +| Поведение экранов | `CRM_bp-gr_Инструкция_v8_5.md` v8.5 | | Админка SaaS (экран 08) | `Админка_SaaS_v8_2.md` | | Открытые вопросы по дизайну | `Открытые_вопросы_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_4.md](docs/CRM_bp-gr_Инструкция_v8_4.md) v8.4 | Главное ТЗ из 28 разделов (финал 06.05.2026) | +| [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) | ## Репозиторий diff --git a/cspell-words.txt b/cspell-words.txt index c28b31a4..c27c65f0 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -689,3 +689,5 @@ soft DEK BYTEA trg +SPE +gethostbyname diff --git a/docs/CRM_bp-gr_Инструкция_v8_4.md b/docs/CRM_bp-gr_Инструкция_v8_5.md similarity index 88% rename from docs/CRM_bp-gr_Инструкция_v8_4.md rename to docs/CRM_bp-gr_Инструкция_v8_5.md index ce332a7d..9fd46390 100644 --- a/docs/CRM_bp-gr_Инструкция_v8_4.md +++ b/docs/CRM_bp-gr_Инструкция_v8_5.md @@ -1,12 +1,64 @@ -# Лидпоток — Полная техническая и функциональная документация (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. + +**8 P0 — реализация в schema.sql v8.5 + правки narrative ниже:** + +- **Биз-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-через-ключи. +- **OPEN-И-20 (§14.X)** — S3 presigned URL TTL 24 ч + триггер `report_jobs_log_export()` AFTER INSERT → `pd_processing_log` action='exported' (152-ФЗ ст.18 ч.2). +- **OPEN-И-21 (§22.X + Прил. И)** — Anti-DDoS стек: Nginx `limit_req_zone` 1000 RPS глобально + Yandex SmartCaptcha на `/register` + disposable-email-blacklist (~50К доменов). +- **Ю-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. + +**Изменённые разделы narrative (in-place ниже):** + +- **§7.1 Карта таблиц** — обновлены счётчики (53→**54 таблицы** +1 `project_user_assignments`, 86→**91 индекс** +5, 33→**34 RLS-политики** +1 + WITH CHECK на 2, 34→**35 ENABLE RLS** +1, 3→**4 роли БД** +1 `crm_audit_writer`, 0→**12 триггеров**, 0→**4 функции**). +- **§10.X (новый «v8.5: Антифрод дублей + автораспределение + scoring + регион + эскалация»)** — Биз-17/19/22/23, CTO-16, OPEN-И-25/26. +- **§12.5.5 Расширенная аналитика** — Биз-18 (TTFR-SLA + alert), CTO-14 (UTM cohort). +- **§14.X (новый «v8.5: Append-only audit hash chain + export log»)** — OPEN-И-15, OPEN-И-20. +- **§17.X (новый «v8.5: Telegram + escalation + late payment alert»)** — Биз-20, Биз-24, OPEN-И-25. +- **§19.10.X (новый «v8.5: DNS-rebinding pin-IP + marketing.conversion»)** — Биз-21, OPEN-И-18. +- **§22.X (новый «v8.5: SSO + SET LOCAL test + Sentry PII + WITH CHECK + Anti-DDoS + TTL secrets + audit hash chain»)** — OPEN-И-13/14/15/16/17/21, Ю-9, CTO-13, CTO-15. +- **§23.10.X (новый «v8.5: Break-glass + two-person impersonation»)** — OPEN-И-13, CTO-15. +- **Прил. И (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**) - `user_recovery_codes`, `user_sessions` — auth-инфраструктура пользователя -- `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**) - `tenant_status_overrides` — переименование настраиваемых статусов - `comment_templates` — шаблоны комментариев - `deal_tags`, `deal_tag_pivot` — теги и связи @@ -1250,12 +1303,13 @@ CREATE TABLE import_log ( - `activity_log` — журнал действий (полный аудит мутаций сделки — см. [§14](#14-журнал-действий-activity-log)) - `webhook_log`, `failed_webhook_jobs` — лог принятых Webhook + упавшие после 3 ретраев джобы - `rejected_deals_log` — лог отвергнутых лидов -- `balance_transactions` — транзакции лидов (+ `chargeback_writedown`, `chargeback_repayment` в v8.1, Ю-3) -- `api_keys` — ключи исходящего REST API +- `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**) - `import_log` — журнал CSV-импорта -- `report_jobs` — асинхронные отчёты +- `report_jobs` — асинхронные отчёты (**v8.5: триггер `report_jobs_log_export` AFTER INSERT → `pd_processing_log`, OPEN-И-20**) **Себестоимость и поставщики (v8.1+, Ю-2):** @@ -1264,15 +1318,15 @@ CREATE TABLE import_log ( **Админка SaaS (v8.1, новый блок):** -- `saas_admin_users` — учётные записи админов SaaS +- `saas_admin_users` — учётные записи админов SaaS (**v8.5: + `sso_provider` (default `yandex360`), + `is_break_glass` для аварийного входа при недоступности IDP, OPEN-И-13**) - `saas_admin_recovery_codes`, `saas_admin_sessions` — auth-инфраструктура админов -- `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. v8.5: Антифрод дублей, автораспределение, scoring, регион, эскалация + +> **Источник:** реестр v1.12 §13.10 (Биз-17/19/22/23, CTO-16, OPEN-И-25/26). + +### 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` дешёв. + +### 10.8.2. Стратегия автораспределения лидов (Биз-17 + CTO-16) + +**Поле `projects.assignment_strategy`:** + +- **`manual` (MVP default)** — паритет с оригиналом. Менеджер выбирается вручную в карточке сделки или массовой операцией (§10.4). +- **`round_robin` (Post-MVP)** — циклически среди active members `project_user_assignments` (skill_filter не применяется). +- **`least_loaded` (Post-MVP)** — менеджер с минимумом открытых сделок проекта. + +**`project_user_assignments(project_id, user_id, skills JSONB, is_active, …)`:** + +- 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. + +### 10.8.3. Lead scoring (Биз-22) + +**Формула без ML:** + +``` +lead_score = supplier.quality_score × (deals.time_in_form_seconds / 60.0) +clamped в [0.00, 99.99] +``` + +**Поля:** + +- `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`). +- `deals.lead_score NUMERIC(5,2)` — заполняется триггером `calc_lead_score()` (BEFORE INSERT/UPDATE OF `time_in_form_seconds`, `project_id`). + +**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` — атрибут сделки для аналитики/фильтра. + +### 10.8.5. Эскалация просроченных лидов (OPEN-И-25) + +**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) + +В `db/schema.sql` секция 17 закомментирован DDL `call_recordings(...)` — задел под Биз-12 Post-MVP (телефонная интеграция + транскрипция). 10 строк DDL + RLS-политика + индекс. Активация — однострочный uncomment + миграция `0001_create_call_recordings.php` при первом клиентском запросе. + +**Поля:** `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 + журнал экспорта + +> **Источник:** реестр v1.12 §13.10 (OPEN-И-15, OPEN-И-20). + +### 14.8.1. Tamper-evident audit log (OPEN-И-15) + +**Проблема:** до 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`. +2. Пересчитывает `expected_hash = sha256(prev_hash || row::text)`. +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`: + +``` +tenant_id := NEW.tenant_id +subject_type := 'lead' -- bulk-выгрузка лидов +subject_id := NULL -- bulk = NULL (cм. CHECK chk_pd_actor) +action := 'exported' +purpose := 'report_job_' +actor_tenant_user_id := NEW.user_id +``` + +**Эффект:** соответствие 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 + +> **Источник:** реестр v1.12 §13.10 (Биз-20, Биз-24, OPEN-И-25). + +### 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 ` → бот сохраняет `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..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 нотификации. +- Голосовые/видео-сообщения. Только текст. +- Telegram Mini-App. Используем стандартный bot API. + +### 17.9.2. Эскалация просроченных лидов — нотификации (OPEN-И-25) + +При срабатывании 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 часа без касания». +2. **Новому менеджеру**: `notification_type='deal.escalated_to_me'`. Каналы: email + in-app + push (срочно). Текст: «Тебе переназначен просроченный лид #1234, контакт: ИвановИП +7900-…». + +**Опционально** (default off): копия админу тенанта при `escalated_count >= 2` — индикация системной проблемы с менеджером. + +### 17.9.3. Late waiting_payment alert (Биз-24) + +**Cron `payments:notify-stale`** (раз в час). + +**Логика:** для каждого `saas_invoices` со `status='waiting_payment'` AND `created_at + INTERVAL '48 hours' < NOW()` AND `notified_finance=FALSE`: + +1. Email finance-роли SaaS (через `saas_admin_users WHERE role='finance' AND is_active`). +2. Bell-нотификация в админке `/admin/billing/stale-invoices` (UI badge на main nav). +3. `saas_invoices.notified_finance = TRUE` (флаг чтобы не дублировать). + +**Дополнительно** (default off): если `created_at + INTERVAL '7 days' < NOW()` — повторный алерт + автоматическое создание `incidents_log` type=`payment_overdue`. + +**Конфигурация:** ключи в `system_settings`: + +- `payments_notify_stale_threshold_hours` (default 48) +- `payments_notify_stale_repeat_hours` (default 168 = 7 дней) +- `payments_notify_stale_enabled` (default true) --- @@ -3353,6 +3635,72 @@ DDL таблиц `outbound_webhook_subscriptions` и `outbound_webhook_deliverie - Запрещены специальные хосты: `localhost`, `metadata.google.internal`, `169.254.169.254` (AWS/GCP/Yandex Cloud metadata). - Проверка SSL-сертификата получателя обязательна (без опции `allow_self_signed`). +### 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` при каждой отправке делает: + +1. **Resolve once** — `gethostbyname()` → получаем IP. +2. **Validate IP** — фильтр RFC1918 + специальные хосты (как в §19.10.10), но **на pinned IP** (не на DNS-имени). +3. **Pin** — открываем HTTP-соединение к `https://` с `Host: ` header (для SSL SNI и virtual host routing). +4. **Connect** — `curl_setopt(CURLOPT_RESOLVE, ["{$hostname}:443:{$pinned_ip}"])`. Это «прибивает» DNS на время сессии — никаких повторных resolve'ов. +5. **Лимит redirect** — `CURLOPT_MAXREDIRS = 3`. При redirect — повторный SSRF-фильтр на новый Location. +6. **Лимит body** — `CURLOPT_RANGE = 0-1048576` (1 MB). Прерываем чтение если получатель попытается отправить нам гигабайт. + +**CA-bundle:** доверенный CA-bundle Mozilla, обновляется в Docker-образе при пересборке. Self-signed запрещены даже на staging. + +**Совместимость:** все существующие тенанты с corretly-configured webhook URLs продолжают работать без изменений. SSRF-фильтр прозрачен для них. + +### 19.10.12. v8.5: marketing.conversion event (Биз-21) + +**Новый 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** — клиент сам пишет адаптер. + --- # 20. SaaS-биллинг (уровень 1: вы → клиенты SaaS) @@ -4693,6 +5041,160 @@ const safe = DOMPurify.sanitize(userInput, SAFE_HTML_CONFIG); - [ ] В `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`. + +**Flow:** + +1. Админ → `/admin/login`. +2. UI отдаёт «Войти через Yandex 360» (primary button) + «Аварийный вход» (secondary, маленький). +3. Yandex 360 → OIDC redirect → callback `/admin/auth/yandex/callback`. +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) { + return app(\App\Services\Sentry\PiiScrubber::class)->scrub($event); +}, +``` + +`App\Services\Sentry\PiiScrubber::scrub()`: + +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). +- Yandex SmartCaptcha: `config/services.php` секция `yandex_captcha` — `client_key`, `server_key`. Frontend Vue компонент `` интегрируется в `` нужных страниц. +- 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 МСК: + +1. Выбирает `api_keys` с `expires_at <= NOW() + INTERVAL '30 days' AND is_active=TRUE`. +2. Группирует по `tenant_id` + `user_id` (владелец ключа). +3. Шлёт email: «Ваш API-ключ `{key_prefix}` истекает через {N} дней. Продлить → {URL}». UI «продлить» в `/api-keys` ставит `expires_at = NOW() + INTERVAL '365 days'` + audit в `activity_log`. + +**Effect:** все секреты проекта имеют TTL ≤ 365 дней. Ротация — proactive (UI «продлить») с напоминанием за 30 дней. + +### 22.13.7. Two-person impersonation + Ю-9 hard-block (CTO-15 + Ю-9) + +**Проблема:** до v8.5 `SaasAdminAuthService::createImpersonationToken()` создавал токен для любого тенанта без проверки compliance-флагов. Если у тенанта `pd_subject_request.processing_restricted=TRUE` (152-ФЗ ст.21 ч.5 — субъект ПДн запретил обработку) — admin SaaS мог несмотря на это войти в кабинет тенанта и просмотреть/изменить данные. Юридически это нарушение. + +**v8.5 решение:** + +`SaasAdminAuthService::createImpersonationToken($tenantId, $reason, $requestedBy)`: + +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`. + +### 22.13.8. Audit append-only hash chain (OPEN-И-15) + +См. подробности в §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'`. + --- # 23. Инфраструктура и деплой @@ -5301,6 +5803,34 @@ DDL `pd_subject_requests` — [`db/schema.sql`](../db/schema.sql) v8.4. > **Полная спецификация админки** — карта экранов, DDL новых таблиц (`saas_admin_users`, `saas_admin_audit_log`), дополнения к чек-листу заказчика, влияние на план спринтов — в [Прил. Г](Админка_SaaS_v8_2.md). Workflow ПДн — в [Прил. Д](Workflow_pd_subject_requests_v8_2.md). +### 23.10.11. v8.5: Yandex 360 SSO + break-glass + two-person impersonation + +> **Источник:** реестр 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.* diff --git a/docs/README_АРХИВ_v8_4.md b/docs/README_АРХИВ_v8_5.md similarity index 94% rename from docs/README_АРХИВ_v8_4.md rename to docs/README_АРХИВ_v8_5.md index 0fe169f5..43b72934 100644 --- a/docs/README_АРХИВ_v8_4.md +++ b/docs/README_АРХИВ_v8_5.md @@ -1,6 +1,6 @@ -# Архив документации Лидпоток (CRM bp-gr) — v8.4 (06.05.2026) +# Архив документации Лидпоток (CRM bp-gr) — v8.5 (07.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). Narrative v8.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.1 (05.05.2026, поздний вечер): аудит связности архива + 13 точечных правок в 5 файлах. Разрешена коллизия `OPEN-И-2` (вариант B′: «контакты эскалации» → `OPEN-И-12` P1). Синхронизирована таблица OPEN-И-* в Runbook v8.2 Часть В. Исправлены 4 ссылки на удалённый `Konspekt_sessii_05_05_2026.md` → `Объединённый_конспект.md`. Помечена 📜 историческая таблица §28 narrative. `Открытые_вопросы` → v1.7. Архитектурных изменений: 0. Состав: 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. --- @@ -51,7 +52,7 @@ v8.0 (25.04.2026) | Файл | Размер | Описание | |---|---|---| -| `CRM_bp-gr_Инструкция_v8_4.md` | ~340 КБ | **Главный файл** — полное ТЗ из 28 разделов. v8.4 = v8.3.1 + переписаны все 13 разделов плана v8.4 (§1, §5, §7, §8, §9, §12, §14, §17, §18.4, §19.10, §22, §23.10, §26). 06.05.2026 — финал. | +| `CRM_bp-gr_Инструкция_v8_5.md` | ~340 КБ | **Главный файл** — полное ТЗ из 28 разделов. v8.4 = v8.3.1 + переписаны все 13 разделов плана v8.4 (§1, §5, §7, §8, §9, §12, §14, §17, §18.4, §19.10, §22, §23.10, §26). 06.05.2026 — финал. | ### Приложения (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-модели, сервисы, миграция, тестирование. @@ -266,14 +267,14 @@ v8.0 (25.04.2026) ### Frontend-разработчик -1. `CRM_bp-gr_Инструкция_v8_4.md` — раздел 26 (UX/Frontend) + шапка v8.3 (CTO-6, CTO-11, JivoSite, click-wrap). +1. `CRM_bp-gr_Инструкция_v8_5.md` — раздел 26 (UX/Frontend) + шапка v8.3 (CTO-6, CTO-11, JivoSite, click-wrap). 2. **`brandbook.md` v1.1 — обязательно**: палитра, типографика, готовая Vuetify 3 тема. **§9.1–9.5** — 5 SVG-исходников логотипа inline (копировать в `/public/assets/`). 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. ### Дизайнер @@ -319,10 +320,10 @@ v8.0 (25.04.2026) | v8.3++ | 05.05.2026 (день) | Параллельный аудит партий 12–15 (4 файла); Прил. М → v1.1 (+§3.5 DDL, +§6.6 security-антипаттерны, +§9 детальная развёртка партий 12–15); Открытые_вопросы → v1.6 (+раздел 12, +Биз-14/15/16, Биз-10 переоткрыт); schema.sql → v8.3; +CHANGELOG-v8_3.md; +конспект сессии 05.05 (2-я часть); окончательный вердикт «outbound webhooks нет» (7 линий доказательств); **21 файл** | | **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 файлов (без изменений в составе).** | --- diff --git a/docs/Runbook_ekspluatatsii_v8_2.md b/docs/Runbook_ekspluatatsii_v8_2.md index 7ee3549b..ca20df65 100644 --- a/docs/Runbook_ekspluatatsii_v8_2.md +++ b/docs/Runbook_ekspluatatsii_v8_2.md @@ -641,8 +641,243 @@ FROM pg_stat_replication; |---|---|---| | 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, ); +-- Ожидание: 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` (раз в сутки) + +**Расписание:** `04:00 МСК` ежедневно. Низкий бизнес-трафик. + +**Логика (псевдокод):** + +```php +// app/Console/Commands/AuditVerifyChain.php +foreach (['auth_log','activity_log','pd_processing_log', + 'saas_admin_audit_log','balance_transactions'] as $table) { + $lastChecked = Cache::get("audit_chain_last_id:$table", 0); + $rows = DB::table($table)->where('id','>',$lastChecked) + ->orderBy('id')->get(); + $prevHash = DB::table($table)->where('id','<=',$lastChecked) + ->orderByDesc('id')->value('log_hash'); + foreach ($rows as $row) { + $expected = hash('sha256', ($prevHash ?? '') . json_encode($row), true); + if ($expected !== $row->log_hash) { + Sentry::captureMessage("audit_chain_break: $table id={$row->id}", 'critical'); + DB::table('incidents_log')->insert([/* type='audit_chain_break' */]); + } + $prevHash = $row->log_hash; + } + Cache::put("audit_chain_last_id:$table", $rows->last()?->id ?? $lastChecked); +} +``` + +**Performance:** инкрементально с `last_id`, полная проверка ~5 минут на 50М строк (одноразово при первой инициализации). + +**При обнаружении break:** + +1. Sentry severity=critical → email админам SaaS + OnCall. +2. `incidents_log` insert type='audit_chain_break', severity='high'. +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. + +## Г.4. OPEN-И-21: Anti-DDoS компоненты + +### Г.4.1. Nginx `limit_req_zone` + +**Файл `nginx/conf.d/ratelimit.conf`:** + +```nginx +limit_req_zone $binary_remote_addr zone=login:10m rate=10r/s; +limit_req_zone $binary_remote_addr zone=api:10m rate=60r/s; +limit_req_zone global zone=global:1m rate=1000r/s; + +server { + location /admin/login { + limit_req zone=login burst=5 nodelay; + limit_req zone=global; + } + location /api/v1/ { + limit_req zone=api burst=20 nodelay; + limit_req zone=global; + } +} +``` + +**Deployment:** через CI/CD job `deploy-nginx-config`. Перед applied — `nginx -t`. После — graceful reload. + +### Г.4.2. Yandex SmartCaptcha + +**Configuration:** `config/services.php` секция `yandex_captcha` — `client_key`, `server_key` (env, в Yandex Lockbox). + +**Frontend:** Vue-компонент `` оборачивает `` страниц `/register`, `/login` (после 2 неудач), `/billing/topup`. + +**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 МСК): + +```bash +curl -fSL https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf \ + -o /var/www/lidpotok/storage/app/disposable-domains.txt +``` + +**Использование:** Laravel validation rule `App\Rules\NotDisposableEmail` на `RegisterController::register()`. Disposable email → ошибка валидации «Используйте корпоративный email». + +## Г.5. OPEN-И-22: Per-tenant DEK + crypto-shred backup + +**Архитектура:** + +- **Yandex Cloud KMS** хранит per-tenant DEK (Data Encryption Key) AES-256. +- При создании tenant'а — `BackupService::createTenantDek($tenantId)` создаёт KMS key с tag `tenant_id=`. +- Backup tenant'а: `pg_dump` → tarball → encryption envelope (random AES key + KMS-DEK) → upload в Object Storage `s3://lidpotok-backups/tenants/{id}/{date}.tar.enc + .envelope`. +- Restore: encrypted envelope → расшифровка через KMS-DEK → расшифровка tarball → pg_restore. + +**Crypto-shred при удалении tenant'а** (после `tenants.deleted_at + 30 days`): + +1. `BackupService::cryptoShred($tenantId)` → `yc kms key destroy --id `. +2. Backup tarballs остаются в Object Storage, но без DEK расшифровать невозможно. +3. Через ещё 60 дней — `lifecycle policy` удаляет физически (defense-in-depth). + +**Преимущество:** 152-ФЗ ст.21 «прекращение обработки» удовлетворяется криптографическим erasure — мгновенно и атомарно. + +## Г.6. OPEN-И-24: pg_anonymizer для staging + +**Цель:** безопасная репликация prod → staging без раскрытия ПДн (152-ФЗ ст.6). + +**Расширение `pg_anonymizer`** ставится в фазе 3 (Прил. Н). + +**Процедура `staging:refresh-from-prod`** (раз в неделю или по запросу): + +```bash +# 1. Backup prod +pg_dump -Fc -h prod-pg.lidpotok.ru -U crm_admin_user lidpotok > prod-snap.dump + +# 2. Restore в staging-db +pg_restore -h staging-pg.lidpotok.ru -U postgres -d lidpotok_staging prod-snap.dump + +# 3. Apply masking +psql -h staging-pg.lidpotok.ru -d lidpotok_staging <= 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'. + +**Конфигурация:** `system_settings` ключи `payments_notify_stale_threshold_hours`/`repeat_hours`/`enabled`. + +--- + +*Драфт v0.3 от 07.05.2026. Часть Г добавлена в рамках реализации v8.5 (коммит `038a884` + narrative v8.5).* diff --git a/web/index.html b/web/index.html index 9800e452..235ec8c4 100644 --- a/web/index.html +++ b/web/index.html @@ -227,7 +227,7 @@