# Админка SaaS — спецификация модуля для v8.2

**Версия:** 8.2 от 04.05.2026 (правки после интервью с заказчиком).

## Что нового в v8.2 относительно v8.1

- ✅ **DO-3 закрыт.** IP allow-list для админки SaaS — **отказались**, т.к. сотрудники работают распределённо. Вместо него — **5 компенсирующих мер безопасности**:
  1. **Rate-limit на `POST /admin/login`**: 5 попыток за 15 минут с одного IP, потом 30-минутная блокировка этого IP. Реализация: Laravel middleware `throttle:5,15`.
  2. **Капча** (Yandex SmartCaptcha — нативно в Yandex Cloud, без трансграничной передачи) появляется после **2 неудачных попыток** на форме логина.
  3. **Email-уведомление админу при входе с нового устройства/IP.** Сравнение по `(user_agent_hash, ip_subnet /24)`. Не блокирует, даёт сигнал. Шаблон: «Вход в админку Лидерра с нового устройства. Если это были не вы — немедленно смените пароль и сообщите команде безопасности».
  4. **Все попытки логина (успешные и неудачные) пишутся в `auth_log`** с полями: `admin_user_id` (NULL если email не найден), `email_attempted`, `ip`, `user_agent`, `result` (`success`/`bad_password`/`bad_2fa`/`rate_limited`), `created_at`. Уже есть в схеме.
  5. **Алерт в Sentry/Slack `#sec-alerts`**, если за час замечено >20 неудачных попыток с разных IP на админку (атака подбором). Метрика собирается из `auth_log`.

- ✅ **DO-5 закрыт.** SSO для админов SaaS — **Yandex 360 (Yandex ID for Business)**. Преимущества:
  - Нативная интеграция с Yandex Cloud (выбран в DO-1);
  - SSO + 2FA встроены, не нужен отдельный TOTP-провайдер;
  - Управление пользователями централизовано в Yandex 360;
  - При увольнении сотрудника — отзыв доступа в одном месте.

  Имплементация: OAuth2 Authorization Code Flow, scope `login:email login:info`. Альтернативный путь — локальный пароль + 2FA — остаётся как fallback на случай недоступности Yandex 360.

- ✅ **Биз-9 закрыт.** **2FA доступна на всех тарифах** (включая `start`). Снято с фичи `pro+`. Соответствующее изменение в `system_settings.feature_flags` и в Прил. Г §4.5.

- ✅ **OPEN-Д-5 / OPEN-И-1: таблица `incidents_log`** добавляется в БД (DDL — в Прил. Д v8.2). Соответствующий экран **«Журнал инцидентов»** в админке — добавляется в раздел 6 этого приложения (роли: `compliance` — read, `super_admin` — read+create).

- ✅ **Биз-7 закрыт (лайт-версия восстановления отвергнутых лидов):** UI «Лог отказанных лидов и история восстановлений» добавляется **только в админку SaaS** (роль `support` + `super_admin`), в кабинете клиента — нет. Экран показывает по тенанту: сколько лидов было отказано, сколько восстановлено при последнем пополнении, какие лиды протухли по 7-дневному TTL.

**Назначение документа:** закрыть пробел 🟠 №5 из конспекта анализа v8.0 — «Админка SaaS упомянута вскользь, не определены экраны, операции, ролевая модель, аутентификация, таблицы». Документ оформлен как **расширение v8.0**, не замена. Все архитектурные решения v8.0 наследуются.

**Статус:** драфт для обсуждения с заказчиком. Решения, требующие подтверждения, помечены `[?]`. Открытые вопросы вынесены в раздел в конце.

**Базовые ссылки на v8.0:**

- 3.4 — SaaS-уровневые сущности (`tenants`, `system_settings`)
- 7.1 — карта таблиц
- 18.6 — удаление аккаунта по 152-ФЗ
- 20.3 — `legal_entities`
- 20.5 — `saas_transactions`
- 20.6 — `refund_requests` (workflow и схема **уже спроектированы**)
- 20.10 — `saas_invoices`, `saas_upd_documents`
- 22.8 — `auth_log` (для входов)
- 22.9 — 152-ФЗ (`tenant_consents`, `pd_processing_log`)
- 27.2 — что нужно от заказчика
- 27.3 — спринт 14 включает «базовую админку SaaS»

---

## 0. Зачем отдельный документ

В v8.0 админка SaaS присутствует фрагментарно:

- Раздел 20.6 описывает workflow возвратов «через админку» и таблицу `refund_requests` с полем `admin_user_id BIGINT` — но **без FK**, потому что таблицы админов нет.
- Раздел 3.4 определяет `system_settings.updated_by BIGINT` — **без FK** по той же причине.
- Раздел 22.9.3 определяет `pd_processing_log.actor_user_id BIGINT` — **без FK**, потому что актором может быть и тенантский user, и админ SaaS.
- Спринт 14 в плане работ выделен под «Поддержка + чат + админка SaaS» с формулировкой «базовая админка SaaS (управление тенантами, ручные начисления, возвраты)» — это половина спринта на всю функциональность.
- Раздел 27.2 («что нужно от заказчика») админку не упоминает вообще.

Этот документ закрывает три ключевых пробела: **(а)** определяет таблицы `saas_admin_users` и `saas_admin_audit_log` и расставляет на них FK везде, где сейчас «висящий BIGINT»; **(б)** описывает экраны и операции; **(в)** добавляет в чек-лист 27.2 пункты, которые заказчик должен подготовить.

---

## 1. Позиционирование

Админка SaaS — отдельное веб-приложение **для сотрудников оператора платформы** (заказчика), **не для клиентов-тенантов**.

**Что админка делает:**

- мониторит платформу (тенанты, нагрузка, очереди, ошибки),
- выполняет ручные операции по биллингу — ручные начисления, возвраты, корректировочные УПД, ручное подтверждение банковских переводов (по 20.9),
- модерирует тенантов (блокировка, разблокировка, удаление по запросу 152-ФЗ — раздел 18.6 v8.0),
- управляет глобальными настройками (`system_settings`, `tariff_plans`, `legal_entities`, `payment_gateways`),
- даёт инструменты разбора инцидентов (просмотр `webhook_log`, `failed_webhook_jobs`, `auth_log`, `pd_processing_log`),
- ведёт работу с обращениями в поддержку — `[OPEN-1]` отдельный модуль или внешний helpdesk; в плане спринтов 14 «чат-виджет» упомянут, но к админке прямо не привязан.

**Что админка НЕ делает:**

- не имеет «по умолчанию» доступа к содержимому сделок тенантов — оператор ПДн лидов = тенант, а не мы (см. 22.9.4); доступ к данным конкретного тенанта — только по основанию, с обязательной записью в `pd_processing_log` и `saas_admin_audit_log`;
- не подменяет SQL-консоль и DBA-инструменты — миграции, ad-hoc запросы, эксплуатация БД остаются у DevOps вне админки;
- не выполняет функции BI — для бизнес-метрик отдельный дашборд (Metabase / Grafana, пробел №14 из конспекта).

---

## 2. Архитектура

### 2.1. Развёртывание

**Решение:** отдельное Laravel-приложение в том же монорепо, отдельный домен `admin.<основной-домен>` (например, `admin.crm-аналог.ru`), общая БД с основным приложением.

**Альтернатива** — отдельные маршруты `/saas-admin/*` внутри основного приложения. Отвергнута по соображениям безопасности и эксплуатации (см. ниже).

**Почему отдельное приложение:**

| Критерий | Отдельное приложение | Маршруты в основном |
|---|---|---|
| Сетевая изоляция | Allow-list по IP на ingress, отдельные WAF-правила | Сложнее, нужно различать на уровне middleware |
| Деплой | Хотфикс админки без перезапуска клиентского приложения | Любая правка перезапускает всё |
| SLA | Админка может быть недоступна 30 мин — клиенты не заметят | Связаны судьбой |
| Аудит безопасности | «Админка = всё в этом домене» | Нужно проверять все маршруты на правильный guard |
| Риск ошибки middleware | Низкий — приложение физически разделено | Высокий — один забытый middleware = эскалация |

**Компромисс:** общий код моделей и миграций через Composer-пакет внутри монорепо (например, `packages/shared-domain`). Это позволяет переиспользовать Eloquent-модели тенантов без копипасты.

**Стек:** PHP 8.3 + Laravel 13, Vue 3 + Vuetify 3 (как у клиентского — переиспользование дизайн-токенов из раздела 26 v8.0). Отдельный `package.json` админки.

### 2.2. Доступ к БД (с учётом CTO-5: RLS включён на MVP)

Схема ролей БД зафиксирована в `schema.sql` v8.1, разделы 12–13:

- **`crm_app_user`** — обычная роль для клиентского приложения. На неё действуют RLS-политики. Перед каждым запросом приложение делает `SET LOCAL app.current_tenant_id = X` в той же транзакции — иначе политика отбрасывает все строки. Это автоматически закрывает риск утечки между тенантами на уровне БД (четвёртый уровень изоляции из v8.0 раздел 22.6, CTO-5).
- **`crm_admin_user`** — отдельная роль для админки SaaS, с **`BYPASSRLS`**. Админка обоснованно работает кросс-тенантно (просмотр всех тенантов, агрегаты, поддержка), поэтому политики не действуют. Это **не «обход global scope через признак на гарде»**, как было в драфте 1.0 — это полноценный механизм PostgreSQL.
- **`crm_migrator`** — роль для миграций, тоже `BYPASSRLS` плюс `CREATEDB`. Нужен для создания новых партиций `deals` и `supplier_lead_costs` и накатывания миграций.

Дополнительные ограничения для `crm_admin_user`:

- `SELECT` — на все таблицы.
- `INSERT, UPDATE` — на `tenants` (для блокировок и статусов), `system_settings`, `tariff_plans`, `legal_entities`, `payment_gateways`, `saas_invoices`, `saas_upd_documents`, `saas_transactions`, `refund_requests`, `balance_transactions` (только тип `manual_adjustment` и `chargeback_*`), `saas_admin_users`, `saas_admin_audit_log`, `tenant_status_overrides`, **`supplier_lead_costs`**, **`supplier_invoices`**, **`impersonation_tokens`**.
- `DELETE` — нигде, кроме `saas_admin_users.deleted_at` (и то soft-маркер).
- **Никаких прав** на физическое чтение зашифрованных полей: `users.password_hash`, `users.totp_secret`, `api_keys.key_hash` — только метаданные.
- **Никаких прав** на `payment_gateways.config` (зашифрованные ключи шлюзов) — только обновление через защищённый интерфейс с явным `Crypt::encrypt()`.
- **Обязательное логирование** каждого write-действия админа в `saas_admin_audit_log` — независимо от того, прошло оно или сломалось.

> **⚠️ Безопасность:** admin-приложение **не должно** использовать ту же роль БД, что и клиентское. Компрометация админ-сессии не должна давать SQL-уровень с правами `DROP/TRUNCATE`. Это требование на уровень PR-ревью.
>
> **Тестирование RLS:** в feature-тестах изоляции (раздел 22.6 v8.0, утилита `assertTenantIsolation`) обязательно выполняется и под `crm_app_user` (политика срабатывает), и проверяется, что без `SET LOCAL app.current_tenant_id` запросы возвращают пустоту.

### 2.3. Аутентификация админов

- **Отдельный guard** `saas_admin` с собственной session-таблицей. Sanctum-токены клиентского приложения не используются.
- **Обязательное 2FA** (TOTP, та же библиотека `pragmarx/google2fa-laravel` что и для клиентов, раздел 18.4.1 v8.0). Без 2FA админ не может пройти второй фактор после ввода пароля. Recovery codes — 8 шт., как у клиентов.
- **Срок жизни сессии:** `[?]` 4 часа idle, 12 часов абсолют. Против 30 дней у клиентов.
- **IP allow-list** на уровне Nginx — список офисных/VPN IP. Дополнительно — per-user IP в `saas_admin_users.allowed_ips` (JSONB). Если поле пусто — действует только глобальный allow-list.
- **Bcrypt cost=12** (как у клиентов), **zxcvbn ≥ 4** (на единицу строже клиентского минимума ≥ 3 из раздела 22.4.1 v8.0).
- **Принудительная смена пароля раз в 90 дней** `[?]` — спорная практика (NIST с 2017 не рекомендует), но в РФ до сих пор требование многих заказчиков.
- **SSO** (Google Workspace / Yandex 360) — `[OPEN-2]`, на MVP не нужно, но архитектурно предусмотреть socialite-driver.
- **Все события входа/выхода/смены пароля/2FA** — в `auth_log` v8.0 раздел 22.8 (расширим существующую таблицу новым типом `actor_type='saas_admin'`, чтобы не плодить вторую — см. п. 5 ниже).

---

## 3. Ролевая модель

Простая RBAC без полноценного ACL — достаточно для команды до 10–15 человек. Роли — фиксированный enum, не настраиваются через UI. Кастомные роли — `[OPEN-3]`, post-MVP.

| Роль | Назначение | Ключевые права |
|------|-----------|---------------|
| `super_admin` | Владелец платформы / CTO | Всё, включая управление другими админами, `system_settings`, выпуск/отзыв ролей |
| `support` | 1-я линия поддержки | Просмотр тенантов, журнал webhook-ов, ручные начисления **до лимита** (см. ниже), смена статуса тарифа, разблокировка после оплаты |
| `finance` | Бухгалтер / финансовый менеджер | Биллинг: подтверждение банковских переводов, начисления, **возвраты**, корректировочные УПД, реестр платежей, выгрузка в 1С |
| `compliance` | DPO / юрист | Просмотр `pd_processing_log`, `tenant_consents`, обработка обращений по 152-ФЗ, экспорт данных по запросу субъекта ПДн |
| `dev_oncall` | Дежурный разработчик | Технические разделы: `failed_webhook_jobs`, очереди Horizon, логи ошибок, healthcheck тенантов; **без** финансовых прав |
| `read_only` | Аудитор / новый сотрудник | Только просмотр всех экранов, без действий |

**Лимиты на ручные операции (anti-fraud, защита от инсайдера):**

- `support` может сделать ручное начисление **до 1000 ₽ или 10 лидов за 24 часа на одного админа** `[?]`. Выше — требует роль `finance`.
- Любая операция на сумму **> 50 000 ₽ требует двойного подтверждения** (four-eyes principle): второй админ подтверждает в течение 24 часов, иначе операция отменяется.
- **Возврат** на любую сумму — только `finance` или `super_admin`, всегда с обязательным основанием в `refund_requests.reason` (требование v8.0 уже есть, я только подтверждаю).

> **⚠️ `[OPEN-4]`** — пороги нужны от заказчика. Цифры — индустриальный бенчмарк. Привязки к сумме лидов в этих лимитах нет, потому что курс ₽/лид зависит от тарифа (см. 21.1 v8.0); используем фиксированные значения и в рублях, и в лидах раздельно.

---

## 4. Карта экранов

Группировка — по бизнес-задачам.

### 4.1. Дашборд

KPI-плитки на сегодня / вчера / неделю:

- активных тенантов / новых регистраций / удалённых,
- MRR / поступления / возвраты,
- объём webhook-ов (получено / обработано / в очереди / failed) — данные из `webhook_log` и `failed_webhook_jobs`,
- ошибки 5xx за последний час (Sentry или внутренний счётчик),
- состояние воркеров Horizon, состояние Redis, состояние БД (replication lag),
- «горящие» алерты: балансы тенантов в минусе, незакрытые УПД старше N дней, повторяющиеся webhook-ошибки.

Виджет «Требует внимания»: список открытых `refund_requests.status='pending'` + ручных подтверждений банковских переводов + возвратов в обработке + просроченные УПД.

### 4.2. Тенанты

**Список:** таблица — id, поддомен, email владельца, тариф, баланс ₽, баланс лидов, статус (`active`, `suspended`, `pending_email_confirm`, `deleted` — все из `tenants.status`, раздел 3.4), создан, последняя активность.

Фильтры: по статусу, тарифу, периоду регистрации, баланс < 0, неактивен > 30 дней (по `last_activity_at`), есть открытые УПД.

**Карточка тенанта:**

- **Шапка:** id, поддомен (с ссылкой), статус, кнопки — `Заблокировать`, `Разблокировать`, **`Войти как…`** (workflow ниже), `Удалить (152-ФЗ)`.
- **Вкладки:**
  - **Профиль** — данные владельца, юр.реквизиты тенанта, юр.лицо оператора по умолчанию (FK на `legal_entities`), согласия 152-ФЗ (выписка из `tenant_consents`).
  - **Биллинг** — текущий тариф, история тарифов (`tariff_subscriptions`, **все 5 статусов: scheduled/active/superseded/expired/cancelled** после CTO-1), баланс ₽ и лиды, **chargeback-долг** (`tenants.chargeback_unrecovered_rub` после Ю-3 — отдельный визуальный блок если > 0), последние 50 транзакций (`saas_transactions` + `balance_transactions`), открытые `saas_invoices`/`saas_upd_documents`, кнопка `Ручное начисление`, `Возврат`.
  - **Webhook** — токен (всегда маскированный, последние 4 символа; полный — отдельной кнопкой `Показать` с записью в audit log), URL `/webhook/{token}`, статус интеграции, последние 100 webhook-ов с фильтром по статусу из `webhook_log`, кнопка `Перезапустить failed`.
  - **Сделки** — **только агрегаты** (count по статусам, без содержимого). По кнопке `Посмотреть конкретную сделку` — модалка с обязательным основанием (текст в свободной форме, минимум 20 символов) → запись в `saas_admin_audit_log` + `pd_processing_log` (`action='viewed'`, `purpose='support_request'`).
  - **Журнал ПДн** — выписка из `pd_processing_log` по этому тенанту.
  - **Аудит админов** — все действия админов SaaS по этому тенанту (выписка из `saas_admin_audit_log` с фильтром `target_tenant_id`).
  - **Impersonation-сессии** — выписка из `impersonation_tokens` по этому тенанту (после Ю-1): кто запрашивал, основание, длительность, действия в сессии.

#### 4.2.1. Workflow «Войти как клиент» (Ю-1, вариант Б — закрыто)

**Требование:** доступно только ролям `support`, `finance`, `super_admin`. Защищено через одноразовый код по email тенанту.

1. Админ в карточке тенанта нажимает «Войти как…».
2. Модалка с обязательным полем **«Основание»** (свободный текст, минимум 30 символов; например: «тикет #1234, клиент жалуется на пропадающие сделки»).
3. Кнопка «Запросить код у клиента». Создаётся запись в `impersonation_tokens`:
   - `requested_by` = текущий админ;
   - `code_hash` = bcrypt 6-значного кода;
   - `reason` = текст основания;
   - `sent_to_email` = snapshot `tenant.contact_email`;
   - `expires_at` = `NOW() + 15 минут`.
4. На `tenant.contact_email` уходит письмо: «Сотрудник поддержки `<email админа>` запросил временный доступ к вашему аккаунту. Основание: `<текст>`. Код: `XXXXXX`. Действителен 15 минут. Если вы не запрашивали поддержку — игнорируйте письмо и сразу смените пароль.»
5. Клиент передаёт код админу любым способом (звонок, мессенджер).
6. Админ вводит код. Система валидирует (не использован, не истёк, совпадает, `failed_attempts < 5`). При успехе — `used_at = NOW()`, создаётся `saas_admin_sessions(impersonating_tenant_id, impersonating_token_id)`, в новой вкладке открывается клиентское приложение в режиме impersonation. Сессия живёт 1 час.
7. После 5 неверных вводов — токен инвалидируется (`failed_attempts = 5`), нужен новый.

**Защиты в режиме impersonation** (HTTP 403 на endpoint-уровне):

- Запрещены: смена пароля, смена email, удаление аккаунта, отзыв всех сессий, удаление сделок и проектов, экспорт всех данных, создание/удаление API-ключей, изменение настроек уведомлений, изменение настроек 2FA, изменение `tenant.contact_email` (это вектор атаки — изменил email, отослал новый код самому себе).
- Разрешены: всё чтение, изменение статусов сделок, добавление комментариев, изменение тегов, изменение reminder-ов, изменение менеджеров.
- Каждое write-действие в impersonation-сессии дополнительно логируется в `saas_admin_audit_log` с признаком `actor_in_impersonation = true` и ссылкой на админа.
- Каждое чтение ПДн лида = запись в `pd_processing_log` с `actor_admin_user_id`, `purpose = 'support_impersonation'`, ссылкой на `impersonation_tokens.id`.

**Принудительный красный баннер** сверху всех экранов: «⚠️ Вы вошли от имени `<организация>` (admin: `<email админа>`, основание: `<текст>`, осталось: `00:34:12`). [Выйти из режима]».

**Email клиенту по завершении сессии** (или таймауту 1 час): «Сессия поддержки от имени вашего аккаунта завершена. Длительность: X минут. Действия администратора: список (например: `обновил статус сделки #1234`, `добавил комментарий к сделке #5678`).»

### 4.3. Биллинг

Подразделы:

1. **Реестр платежей** — все `saas_transactions`. Фильтры: шлюз, статус, сумма, тенант, период, **`failure_reason`** (после CTO-3 — фильтр по причинам отказа: `gateway_canceled`, `gateway_declined`, `gateway_create_timeout`, `hard_timeout_24h`, `cron_polling_failed`, `late_webhook_ignored`). Экспорт XLSX/1C `[OPEN-6]` (формат для 1С — нужно от заказчика, Б-2).
2. **Реестр УПД** — все `saas_upd_documents`, статусы (`draft`, `issued`, `cancelled`), номера, скачать PDF.
3. **Корректировочные УПД** — отдельный workflow: причина (возврат / коррекция суммы / коррекция реквизитов), привязка к исходному УПД (`refund_requests.correction_upd_id`), генерация номера в `document_sequences` с FOR UPDATE.
4. **Возвраты (`refund_requests`)** — workflow в 20.6 v8.0 + Ю-3:
   - Подвкладка **«Наши возвраты»** (`source='admin_initiated'`): список pending, экран обработки, кнопки `Одобрить` / `Отклонить`, отображение результата `gateway.refund()`. Если сумма > порога ДЕФ-4 (50 000 ₽) — обязательное `four-eyes` подтверждение от второго админа.
   - Подвкладка **«Внешние возвраты / chargeback»** (`source='external_chargeback'`): список авто-созданных записей из webhook `refund.succeeded`. Информационный режим (статус сразу `processed`), но видны связанные алерты по `tenants.chargeback_unrecovered_rub > 0`. Кнопка **«Списать как убыток»** (`balance_transactions(type='manual_adjustment', amount = chargeback_unrecovered_rub)`, обнуление поля, восстановление статуса `active`) — требует four-eyes если сумма > порога. Каждая операция — в `saas_admin_audit_log`.
5. **Ручные подтверждения банковских переводов** — для сценария 20.9 v8.0 (юрлицо платит по счёту, оплата приходит на р/с — нужно вручную пометить `saas_invoices.status='paid'` и пополнить баланс).
6. **Ручные начисления и корректировки** (`balance_transactions.type='manual_adjustment'` — из 21.1 v8.0). Обязательное основание, обязательный тенант, лимиты по роли (см. §3).
7. **Алерты** (после CTO-3 + Ю-3):
   - «Поздний webhook от шлюза по failed-транзакции» — список `saas_transactions` с `failure_reason='late_webhook_ignored'`. `finance` решает: возврат через шлюз или ручное начисление.
   - «Тенант приостановлен из-за непокрытого chargeback» — список `tenants` с `status='suspended' AND chargeback_unrecovered_rub > 0`.
   - «Pending транзакции старше 30 минут» — для мониторинга работы cron `payments:cancel-stale`.

### 4.4. Тарифы и юр.лица

- **`tariff_plans`** (CRUD) — структура из 20.2.1 v8.0.
- **`legal_entities`** (CRUD) — структура из 20.3 v8.0.
- **`payment_gateways`** — конфигурация шлюзов (раздел 20.4.2). Секреты (api_key, secret) — отображаются маскированными, изменение — только super_admin, с обязательным переподтверждением паролем.

### 4.5. Глобальные настройки (`system_settings`)

CRUD по таблице из 3.4. Часть ключей — критичные (`trial_bonus_leads`, `inactive_delete_months`), их изменение — только super_admin, с подтверждением паролем и записью в audit log.

При изменении — проставлять `system_settings.updated_by` = id текущего админа SaaS (после введения FK — см. п. 5).

### 4.6. Очереди и инциденты

- **Failed webhook jobs** — список из `failed_webhook_jobs` с возможностью retry / отбросить.
- **Очередь Horizon** — встроить дашборд Horizon (он уже есть в Laravel) с защитой через guard `saas_admin`.
- **Логи Sentry** — интеграция или ссылка наружу (внешний дашборд).

### 4.7. 152-ФЗ / Compliance

- **Журнал `pd_processing_log`** — фильтры по тенанту, типу действия, периоду, актору.
- **Журнал `tenant_consents`** — выписки.
- **Запросы субъектов ПДн** — формализованный workflow обработки запроса физлица об удалении/выгрузке его данных. **`[OPEN-7]`** — таблица `pd_subject_requests` нужна, в v8.0 её нет.
- **Аудит самой админки** — `saas_admin_audit_log`.

### 4.8. Управление админами SaaS

CRUD по `saas_admin_users` — только super_admin. Назначение/смена ролей, IP allow-list, сброс 2FA (с обязательным основанием и логированием), force logout.

### 4.9. Поставщики и себестоимость (новый раздел после Ю-2)

После закрепления реселлерской модели — отдельный раздел админки для управления закупкой лидов у crm.bp-gr.ru.

**Подразделы:**

1. **Дашборд маржи** — общая выручка / себестоимость / валовая прибыль за период (день / неделя / месяц / квартал). Разбивка:
   - по тенантам (топ-20 «прибыльных» и топ-20 «убыточных»);
   - по проектам (если у разных проектов будут разные тарифы — на v2);
   - по тарифным планам (`start` / `basic` / `pro` / `enterprise`).
   Источник: `JOIN balance_transactions` (выручка) с `supplier_lead_costs` (себестоимость) через `deal_id`.

2. **Реестр закупок** (`supplier_lead_costs`) — таблица с фильтрами по периоду / тенанту / проекту, сортировка по `received_at`. Экспорт XLSX. Каждая строка показывает: дата, тенант, deal_id, цена закупки, цена продажи (из `balance_transactions`), маржа.

3. **Реестр счетов от поставщика** (`supplier_invoices`):
   - **Форма ввода счёта**: финансист загружает PDF от crm.bp-gr.ru → вводит вручную: `invoice_number`, период, кол-во лидов по счёту, сумма по счёту.
   - **Кнопка «Сверить»**: автоматически считает `leads_count_actual = COUNT(supplier_lead_costs WHERE received_at BETWEEN period_from AND period_to)` и `amount_rub_actual = SUM(cost_rub)`. Поле `discrepancy_rub` (generated column) показывает расхождение. При расхождении > 1% — визуальный алерт.
   - **Действия**: `Сверено` (статус → `reconciled`, `reconciled_by` = текущий админ); `Оплачено` (статус → `paid`, `paid_at`, `paid_by`); `Спорить` (статус → `disputed`, обязательное поле `notes` с обоснованием).

4. **Алерты для `finance`** (после Ю-2):
   - **Отрицательная маржа на тенанте** за месяц (выручка < себестоимость). Обычно означает: тариф клиента дешевле закупки. Действие: пересмотр тарифа клиента или индивидуальные условия (`tariff_subscriptions.custom_overrides`).
   - **Расхождение в счёте от поставщика** > порога (например, 1% от суммы). Действие: разбор с crm.bp-gr.ru, при необходимости статус `disputed`.
   - **Новый счёт от поставщика старше 5 рабочих дней без обработки** — напоминание.

5. **Управление закупочной ценой** (`system_settings.supplier_default_cost_rub`):
   - Изменение этого ключа через стандартный экран `system_settings`, но с дополнительным полем «Дата вступления в силу» (по умолчанию = `NOW()`, можно отложить на будущее).
   - При изменении — обязательное поле «Причина изменения» (например: «новые условия договора с crm.bp-gr.ru с 1 июня»). Запись в `saas_admin_audit_log`.
   - **Историческая себестоимость не пересчитывается** — только новые лиды получают новую цену. Старые `supplier_lead_costs` хранят snapshot цены на момент получения.

**Доступ:** разделы 1, 2, 3 — `finance`, `super_admin`. Разделы 4, 5 — `super_admin` (и `finance` с подтверждением четырёх глаз для изменения цены).

---

## 5. Схемы таблиц

### 5.1. `saas_admin_users` — учётные записи админов SaaS

```sql
CREATE TABLE saas_admin_users (
    id                  BIGSERIAL PRIMARY KEY,
    email               VARCHAR(255) UNIQUE NOT NULL,
    full_name           VARCHAR(255) NOT NULL,
    password_hash       VARCHAR(255) NOT NULL,           -- bcrypt cost=12
    role                VARCHAR(20) NOT NULL,            -- super_admin, support, finance, compliance, dev_oncall, read_only
    is_active           BOOLEAN DEFAULT TRUE,
    -- 2FA (обязательно для всех)
    totp_secret_enc     TEXT,                            -- зашифровано Crypt::encryptString
    totp_enabled_at     TIMESTAMPTZ,
    -- IP allow-list (per-user, дополняет глобальный)
    allowed_ips         JSONB,                           -- ["10.0.0.0/8", "1.2.3.4"]
    -- Безопасность
    password_changed_at TIMESTAMPTZ DEFAULT NOW(),
    failed_login_count  INT DEFAULT 0,
    locked_until        TIMESTAMPTZ,
    -- Аудит
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    created_by          BIGINT REFERENCES saas_admin_users(id),
    last_login_at       TIMESTAMPTZ,
    deleted_at          TIMESTAMPTZ,                     -- soft delete
    CONSTRAINT chk_role CHECK (role IN ('super_admin','support','finance','compliance','dev_oncall','read_only'))
);

CREATE INDEX idx_saas_admin_users_active ON saas_admin_users(is_active) WHERE deleted_at IS NULL;
```

**Recovery codes** для 2FA — отдельная таблица `saas_admin_recovery_codes` по аналогии с `user_recovery_codes` v8.0 (структура такая же, FK на `saas_admin_users` вместо `users`).

**Sessions** — отдельная таблица `saas_admin_sessions` по аналогии с `user_sessions` v8.0, с тем же набором полей плюс `ip_address INET NOT NULL` (для проверки allow-list при каждом запросе).

### 5.2. `saas_admin_audit_log` — журнал действий админов

```sql
CREATE TABLE saas_admin_audit_log (
    id                  BIGSERIAL PRIMARY KEY,
    admin_user_id       BIGINT NOT NULL REFERENCES saas_admin_users(id),
    action              VARCHAR(100) NOT NULL,           -- 'tenant.suspend', 'refund.approve', 'system_settings.update', ...
    target_type         VARCHAR(50),                     -- 'tenant', 'saas_transaction', 'system_setting', 'saas_admin_user', ...
    target_id           BIGINT,
    target_tenant_id    BIGINT REFERENCES tenants(id),   -- денормализация для быстрого фильтра в карточке тенанта
    payload_before      JSONB,                           -- состояние до (для UPDATE)
    payload_after       JSONB,                           -- состояние после
    reason              TEXT,                            -- обязательное основание для критичных действий
    ip_address          INET NOT NULL,
    user_agent          TEXT,
    requires_approval   BOOLEAN DEFAULT FALSE,           -- four-eyes flag
    approved_by         BIGINT REFERENCES saas_admin_users(id),
    approved_at         TIMESTAMPTZ,
    created_at          TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_admin_audit_admin   ON saas_admin_audit_log(admin_user_id, created_at DESC);
CREATE INDEX idx_admin_audit_tenant  ON saas_admin_audit_log(target_tenant_id, created_at DESC) WHERE target_tenant_id IS NOT NULL;
CREATE INDEX idx_admin_audit_action  ON saas_admin_audit_log(action, created_at DESC);
CREATE INDEX idx_admin_audit_pending ON saas_admin_audit_log(approved_at) WHERE requires_approval = TRUE AND approved_at IS NULL;
```

**Retention:** 5 лет (для финансовых операций — требование 402-ФЗ «О бухгалтерском учёте»). Архивация в S3 после 1 года.

### 5.3. Расстановка FK на существующие таблицы v8.0

Эти изменения нужно внести в v8.0 при сборке v8.1:

| Таблица | Поле | Было в v8.0 | Стало в v8.1 |
|---------|------|-------------|--------------|
| `system_settings` | `updated_by` | `BIGINT` без FK | `BIGINT REFERENCES saas_admin_users(id)` |
| `refund_requests` | `admin_user_id` | `BIGINT` без FK | `BIGINT NOT NULL REFERENCES saas_admin_users(id)` |
| `pd_processing_log` | `actor_user_id` | `BIGINT` без FK | разделить на два поля: `actor_tenant_user_id BIGINT REFERENCES users(id)` и `actor_admin_user_id BIGINT REFERENCES saas_admin_users(id)`, с CHECK что заполнено ровно одно |

> **Альтернатива** для `pd_processing_log` — добавить столбец `actor_type VARCHAR(20)` (`tenant_user` / `saas_admin`) и оставить `actor_user_id` без FK. Решение зависит от того, насколько строго хотим целостность — рекомендую раздельные поля, потому что денормализация в `pd_processing_log` приоритетнее аккуратности.

### 5.4. Расширение `auth_log` (раздел 22.8 v8.0)

Сейчас `auth_log` имеет `user_id BIGINT REFERENCES users(id)` — то есть только тенантские пользователи. Чтобы не плодить вторую таблицу, расширим:

```sql
ALTER TABLE auth_log ADD COLUMN actor_type VARCHAR(20) NOT NULL DEFAULT 'tenant_user';  -- 'tenant_user' | 'saas_admin'
ALTER TABLE auth_log ADD COLUMN saas_admin_user_id BIGINT REFERENCES saas_admin_users(id);
ALTER TABLE auth_log ADD CONSTRAINT chk_auth_log_actor CHECK (
    (actor_type = 'tenant_user' AND user_id IS NOT NULL AND saas_admin_user_id IS NULL)
    OR (actor_type = 'saas_admin' AND saas_admin_user_id IS NOT NULL AND user_id IS NULL)
);
```

`tenant_id` для админов остаётся NULL (они вне тенантов).

### 5.5. `pd_subject_requests` — обращения субъектов ПДн (новая таблица)

Закрывает пробел 4.7 (см. выше). Нужна для compliance-роли.

```sql
CREATE TABLE pd_subject_requests (
    id                  BIGSERIAL PRIMARY KEY,
    received_at         TIMESTAMPTZ DEFAULT NOW(),
    subject_email       VARCHAR(255),                    -- если известен
    subject_phone       VARCHAR(20),
    subject_full_name   VARCHAR(255),
    request_type        VARCHAR(30) NOT NULL,            -- 'access', 'rectification', 'deletion', 'objection'
    description         TEXT,
    status              VARCHAR(20) DEFAULT 'received',  -- received, in_progress, completed, rejected
    -- Связь с тенантом (если запрос относится к лиду в конкретном тенанте)
    tenant_id           BIGINT REFERENCES tenants(id),
    -- Обработка
    assigned_admin_id   BIGINT REFERENCES saas_admin_users(id),
    response_sent_at    TIMESTAMPTZ,
    response_text       TEXT,
    -- Срок ответа по 152-ФЗ — 30 дней
    deadline_at         TIMESTAMPTZ GENERATED ALWAYS AS (received_at + INTERVAL '30 days') STORED,
    completed_at        TIMESTAMPTZ
);
```

---

## 6. Дополнения к чек-листу 27.2 v8.0

В таблице «Что нужно от заказчика» добавить пункты:

| № | Что нужно | Срок | Кто отвечает |
|---|-----------|------|--------------|
| 12 | **Список сотрудников оператора SaaS** с распределением по ролям (`super_admin`, `support`, `finance`, `compliance`, `dev_oncall`, `read_only`) — ФИО, email, телефон | До спринта 14 | Заказчик |
| 13 | **IP allow-list** — список постоянных IP офиса / VPN, с которых будут заходить админы | До спринта 14 | Заказчик + DevOps |
| 14 | **Политика лимитов** на ручные операции — пороги для роли `support` (предложение ниже), порог для four-eyes (предложение: 50 000 ₽) | До спринта 14 | Заказчик |
| 15 | **Решение по «Войти как…»** — запретить, разрешить с тикетом, разрешить полностью (с DPO-юристом) | До спринта 14 | Заказчик + юрист |
| 16 | **Формат выгрузки в 1С** — версия 1С, формат файла (XML / DBF / CSV), требуемые поля | До спринта 8 | Заказчик + бухгалтер |

---

## 7. Влияние на план спринтов

В плане v8.0 (раздел 27.3) спринт 14 объединяет «Поддержка + чат + базовая админка SaaS». Реалистичная оценка с учётом этого документа:

- **Базовая админка** (тенанты-список, тенанты-карточка, биллинг-просмотр, ручные начисления, обработка возвратов через существующий `refund_requests`, аутентификация админов с 2FA, audit log) — **полный спринт 14**.
- **Расширенная админка** (compliance-вкладки, `pd_subject_requests`, журналы инцидентов, four-eyes workflow, выгрузка в 1С, корректировочные УПД UI) — **+1 спринт**, можно разместить как 14a между 14 и 15 либо вынести в post-MVP.
- **Чат-виджет / helpdesk** — отдельный 0.5 спринта или внешний сервис (Carrot Quest, JivoSite). Рекомендация: **внешний сервис на MVP**, экономит спринт.

**Итого:** на MVP админка занимает не половину, а **полный спринт 14**, плюс чат-виджет вынесен на внешний сервис. Календарь сдвигается на 0 (объём тот же), но реалистичность выше.

---

## 8. Открытые вопросы (статус после P0-блока)

| ID | Вопрос | Кому | Статус |
|----|--------|------|--------|
| OPEN-1 | Helpdesk — внутренний чат-виджет или внешний (Carrot Quest, JivoSite, etc.) | Заказчик | `[OPEN]` P1 — до спринта 14 (Биз-5) |
| OPEN-2 | SSO для админов на MVP (нужен / не нужен) | Заказчик | `[OPEN]` P2 — Post-MVP по умолчанию (DO-5) |
| OPEN-3 | Кастомные роли админов через UI (нужны на MVP / post-MVP) | Заказчик | Post-MVP по умолчанию (ДЕФ-8) |
| OPEN-4 | Конкретные пороги лимитов на ручные операции | Заказчик | `[OPEN]` дефолты ДЕФ-3, ДЕФ-4 предложены |
| OPEN-5 | «Войти как…» — да/нет/с тикетом | Заказчик + юрист | ✅ закрыт **Ю-1: вариант Б** (одноразовый код по email тенанту, 15 мин, 5 попыток, сессия 1 час) |
| OPEN-6 | Формат выгрузки в 1С | Заказчик + бухгалтер | `[OPEN]` P0 (Б-2) — до спринта 8 |
| OPEN-7 | Workflow обработки запросов субъектов ПДн | Заказчик + юрист | `[OPEN]` — отдельная задача после P0-блока |
| OPEN-8 | Принудительная смена пароля админов раз в 90 дней — да/нет | Заказчик | `[OPEN]` — дефолт ДЕФ-2 предложен |

**Итог:** из 8 OPEN — 1 закрыт (OPEN-5), для 4 предложены дефолты (OPEN-2, 3, 4, 8), 3 остаются (OPEN-1, 6, 7).

---

## 9. Метки для сверки с заказчиком

`[?]` в документе — пункты, где я предложил значение по умолчанию, но оно требует подтверждения:

- срок жизни сессии админа (4ч idle / 12ч абсолют) — ДЕФ-1;
- ротация пароля 90 дней — ДЕФ-2;
- лимиты ручного начисления для `support` (1000 ₽ / 10 лидов на 24ч) — ДЕФ-3;
- порог four-eyes (50 000 ₽) — ДЕФ-4.

---

*Версия: 1.1 от 03.05.2026 (после P0-блока: CTO-1…5, Биз-1, Биз-2, Ю-1, Ю-2, Ю-3, CTO-4).*
