# MVP Spec 04 — Admin Roles & Item/Review Moderation

## Паспорт документа

- Статус документа: working spec
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: перед реализацией, при изменении domain rules или при смене source-of-truth по контракту
- Область применения: детальные feature-спеки для product backlog и реализации MVP/v1 направлений
- Связанные документы:
  - [Product roadmap и delivery checklist](../product-roadmap.md)
  - [Roadmap](../../project/roadmap.md)
  - [Карта API-маршрутов](../../architecture/api-routes.md)

> Version: MVP · Priority: P0 · Phase: A (Supply)
> Status: Draft v1

---

## 1. Контекст и цель

Административная панель — инструмент внутренней команды Qadam для обеспечения качества контента, соблюдения правил платформы и управления пользователями.

**Три ключевые проблемы, которые решает этот модуль:**

1. **Критичный баг с видимостью:** айтемы в статусах `pending`, `draft`, `rejected`, `revision_required`, `archived` появляются в публичном каталоге. После реализации этого модуля в каталоге отображаются ТОЛЬКО `active` айтемы с `item_isvisible = true`.
2. **Нет ролевой модели:** единственная роль `ADMIN` без разграничения обязанностей. Этот модуль вводит четыре роли с разными уровнями доступа.
3. **Нет модерации отзывов:** продавец не может оспорить отзыв, который нарушает правила.

**Цель модуля:** дать команде Qadam инструменты для:
- Создания и управления аккаунтами администраторов с разными ролями
- Проверки и принятия решений по айтемам продавцов (approve/reject/revision)
- Рассмотрения жалоб продавцов на отзывы
- Управления аккаунтами пользователей (block/unblock/send to review)

**Что не входит в этот модуль:**
- Создание айтемов продавцами → Spec 02
- Публичный каталог и поиск → Spec 05
- Управление отзывами со стороны покупателя → Spec 14
- Жалоба продавца на отзыв (триггер для UC-06) → Spec 14
- Аналитика и дашборд для Analyst → Spec 11 (seller), отдельная задача
- Reference data (subjects, locations) — Spec 16

---

## 2. Роли пользователей

### Роли администраторов платформы

| Роль | Кодовое имя | Доступы |
|------|-------------|---------|
| **Root** | `root` | Полный доступ ко всему. Создаёт других администраторов. Единственный, кто может назначать/менять роли. |
| **Верификатор** | `verifier` | Модерация айтемов + модерация отзывов |
| **Аналитик** | `analyst` | Дашборд + все лиды + просмотр пользователей (без блокировки) |
| **Маркетолог** | `marketer` | Управление reference data (subjects, locations) + просмотр пользователей (без блокировки) |

### Матрица доступов

| Действие | Root | Верификатор | Аналитик | Маркетолог |
|----------|------|-------------|----------|------------|
| Создать/удалить admin | ✅ | ❌ | ❌ | ❌ |
| Изменить роль admin | ✅ | ❌ | ❌ | ❌ |
| Модерация айтемов | ✅ | ✅ | ❌ | ❌ |
| Модерация отзывов | ✅ | ✅ | ❌ | ❌ |
| Блокировка пользователей | ✅ | ❌ | ❌ | ❌ |
| Просмотр пользователей | ✅ | ❌ | ✅ | ✅ |
| Дашборд + все лиды | ✅ | ❌ | ✅ | ❌ |
| Reference data (CRUD) | ✅ | ❌ | ❌ | ✅ |

---

## 3. Use Cases

---

### UC-01: Root создаёт аккаунт администратора

**Актор:** Root (авторизованный admin с ролью `root`)
**Предусловие:** Root авторизован, находится в /admin
**Триггер:** Root нажимает "+ Добавить администратора" в /admin/team

**Полный поток:**

```
[Точка входа]
→ Root в навигации /admin нажимает раздел "Команда" → /admin/team
→ Видит список всех администраторов
→ Нажимает "+ Добавить администратора"
→ Открывается модал "Новый администратор"

─────────────────────────────────────────────────────────
ФОРМА СОЗДАНИЯ АДМИНИСТРАТОРА
─────────────────────────────────────────────────────────
→ Поля:
    - Имя * (2–50 символов)
    - Фамилия * (2–50 символов)
    - Email * (будет логином)
    - Роль * (radio):
        ◉ Верификатор — модерация айтемов и отзывов
        ○ Аналитик — дашборд, лиды, просмотр пользователей
        ○ Маркетолог — справочные данные, просмотр пользователей

→ Root нажимает "Создать"
→ Система:
    1. Создаёт Account { account_type: ADMIN, account_status: active }
    2. Создаёт AdminProfile { admin_role: выбранная роль }
    3. Генерирует временный пароль
    4. Отправляет письмо на email:
       "Вы добавлены в команду Qadam.
       Ваша роль: {роль}.
       Войдите: https://qadam.uz/login
       Логин: {email}
       Временный пароль: {temp_password}"
→ Toast (зелёный): "Администратор создан. Приглашение отправлено на {email}."
→ Новый admin появляется в списке /admin/team
```

---

### UC-01 — Альтернативные потоки и обработка ошибок

**1a. Email уже зарегистрирован в системе:**
```
Триггер: POST возвращает 409 EMAIL_TAKEN

UI-реакция:
→ Поле email: красная обводка + ⚠
→ Под полем: "Этот email уже используется другим аккаунтом на платформе."
→ Форма не закрывается
```

**1b. Обязательное поле не заполнено:**
```
UI-реакция:
→ Пустые поля: красная обводка + ⚠
→ Под каждым: "Это поле обязательно для заполнения"
→ Форма не отправляется
```

**1c. Роль не выбрана:**
```
UI-реакция:
→ Блок с radio-кнопками: красная обводка
→ Под блоком: "Выберите роль для нового администратора"
```

**1d. Ошибка отправки email (SMTP недоступен):**
```
Поведение:
→ Аккаунт СОЗДАЁТСЯ (запись в БД сохранена)
→ Toast (жёлтый): "Администратор создан, но письмо-приглашение не отправлено.
  Передайте пароль вручную или повторите отправку."
→ В карточке admin: кнопка "Повторно отправить приглашение"
```

**1e. Root пытается создать другого Root:**
```
UI-реакция:
→ Роль "Root" не присутствует в списке radio-кнопок
→ Через API: 403 { error: 'CANNOT_CREATE_ROOT', message: 'Роль Root нельзя создать через интерфейс.' }
```

---

### UC-02: Верификатор просматривает очередь модерации айтемов

**Актор:** Верификатор или Root
**Предусловие:** Admin авторизован
**Триггер:** Admin переходит на /admin/moderation/items

**Полный поток:**

```
[Точка входа]
→ Admin в навигации нажимает "Модерация" → открывается /admin/moderation/items
→ Страница показывает очередь: список айтемов со статусом `pending`
  Отсортировано по дате отправки: старые сначала (FIFO)

─────────────────────────────────────────────────────────
СПИСОК ОЧЕРЕДИ МОДЕРАЦИИ
─────────────────────────────────────────────────────────
→ Каждая строка таблицы содержит:
    - Название айтема
    - Продавец (org_name)
    - Дата отправки на модерацию
    - Это повторная модерация? (бейдж "Повторно" если статус менялся ранее)
    - Кнопка "Рассмотреть"

→ Фильтры вверху:
    - Показывать: Все pending / Первичные / Повторные
    - Сортировка: Старые сначала / Новые сначала

→ Счётчик: "В очереди: 47 айтемов"

→ Пустое состояние (если очередь пуста):
  Иллюстрация + "Очередь пуста. Все айтемы проверены ✓"
```

---

### UC-02 — Альтернативные потоки

**2a. Нет доступа (роль Аналитик или Маркетолог):**
```
UI-реакция:
→ Раздел "Модерация" не отображается в навигации
→ При прямом переходе на /admin/moderation/items:
   Страница 403: "У вас нет доступа к этому разделу."
```

---

### UC-03: Верификатор одобряет айтем

**Актор:** Верификатор или Root
**Предусловие:** Айтем существует, moderation_status = `pending`
**Триггер:** Admin нажимает "Рассмотреть" на строке айтема

**Полный поток:**

```
[Точка входа]
→ В /admin/moderation/items нажимает "Рассмотреть"
→ Открывается /admin/moderation/items/[item_id]
→ Страница детального просмотра айтема:

─────────────────────────────────────────────────────────
ПРОСМОТР АЙТЕМА НА МОДЕРАЦИИ
─────────────────────────────────────────────────────────
Левая колонка — полная карточка айтема:
    - Название, описание, все параметры
    - Цены и варианты оплаты
    - Фото и видео
    - Преподаватели (если привязаны)
    - Профиль продавца (org_name, статус, история нарушений)

Правая колонка — панель решения:
    [✅ Одобрить]
    [✏ Отправить на доработку]
    [❌ Отклонить]

─────────────────────────────────────────────────────────
ДЕЙСТВИЕ: ОДОБРИТЬ
─────────────────────────────────────────────────────────
→ Admin нажимает "✅ Одобрить"
→ Диалог подтверждения:
  "Одобрить айтем «{название}»?
  Он появится в каталоге немедленно."
  [Отмена] [Одобрить]

→ При подтверждении:
    - Item.moderation_status = active
    - Item.item_isvisible = true (если не был скрыт продавцом вручную)
    - Item.moderation_comment = null (сбрасываем старый комментарий)
    - Item.moderated_by = admin_id
    - Item.moderated_at = now()
→ Айтем исчезает из очереди
→ Toast (зелёный): "Айтем одобрен и опубликован в каталоге."
→ Возврат к списку очереди /admin/moderation/items
→ (Фоново) Продавец получает уведомление: "Ваш курс «{название}» прошёл проверку и опубликован."
```

---

### UC-03 — Альтернативные потоки

**3a. Технический сбой при сохранении решения:**
```
UI-реакция:
→ Toast (красный): "Не удалось сохранить решение. Попробуйте ещё раз."
→ Статус айтема не изменяется
→ Кнопки решения снова активны
```

**3b. Айтем уже промодерирован другим верификатором (race condition):**
```
Триггер: PATCH возвращает 409 ITEM_ALREADY_MODERATED

UI-реакция:
→ Toast (жёлтый): "Этот айтем уже промодерирован другим верификатором. Статус: {active/rejected}."
→ Страница обновляет статус айтема
→ Кнопки решения блокируются
```

---

### UC-04: Верификатор отклоняет айтем с комментарием

**Актор:** Верификатор или Root
**Предусловие:** Айтем существует, moderation_status = `pending`
**Триггер:** Admin нажимает "❌ Отклонить" в панели решения

**Полный поток:**

```
[Точка входа]
→ Admin находится на /admin/moderation/items/[item_id]
→ Нажимает "❌ Отклонить"
→ Панель разворачивается (или появляется модал):

─────────────────────────────────────────────────────────
ФОРМА ОТКЛОНЕНИЯ
─────────────────────────────────────────────────────────
→ Причина отклонения * (обязательно):
    - Выбор из шаблонов (dropdown или radio):
        ○ Неполное описание курса
        ○ Нарушение правил платформы
        ○ Недостоверная информация (ложные обещания)
        ○ Запрещённый контент
        ○ Дублирующий айтем
        ○ Другое (требует текста)
    - Текстовое поле "Комментарий для продавца" * (обязательно):
        placeholder: "Опишите, что именно не соответствует требованиям..."
        max 1000 символов

→ Чекбокс: "Отправить продавцу уведомление" (default: включён)

→ Кнопка "Подтвердить отклонение"

─────────────────────────────────────────────────────────
ПОСЛЕ ПОДТВЕРЖДЕНИЯ
─────────────────────────────────────────────────────────
→ Item.moderation_status = rejected
→ Item.moderation_comment = {текст комментария}
→ Item.item_isvisible = false (принудительно)
→ Item.moderated_by = admin_id
→ Item.moderated_at = now()
→ Продавец видит в /seller/items: бейдж "Отклонён" + комментарий
→ Toast (зелёный): "Айтем отклонён. Продавец уведомлён."
→ Возврат к /admin/moderation/items
```

**Что видит продавец после отклонения:**
```
В /seller/items на карточке айтема:
    Бейдж: [Отклонён]
    Блок с причиной (раскрывающийся):
    "Ваш курс отклонён.
    Причина: {shablон}
    Комментарий: {текст от верификатора}
    Исправьте указанные недостатки и отправьте курс на повторную проверку."
    Кнопка: "Редактировать и отправить повторно"
```

---

### UC-04 — Альтернативные потоки

**4a. Комментарий не заполнен (нажали "Подтвердить" без текста):**
```
UI-реакция:
→ Поле комментария: красная обводка + ⚠
→ Под полем: "Укажите причину отклонения для продавца"
→ Форма не отправляется
```

**4b. Комментарий превышает 1000 символов:**
```
UI-реакция:
→ Счётчик под полем: "987/1000" → при превышении: красный "1023/1000"
→ Поле: красная обводка
→ Под полем: "Максимум 1000 символов"
```

---

### UC-05: Верификатор отправляет айтем на доработку

**Актор:** Верификатор или Root
**Предусловие:** Айтем существует, moderation_status = `pending`
**Триггер:** Admin нажимает "✏ Отправить на доработку"

**Полный поток:**

```
[Точка входа]
→ Admin находится на /admin/moderation/items/[item_id]
→ Нажимает "✏ Отправить на доработку"
→ Панель разворачивается:

─────────────────────────────────────────────────────────
ФОРМА "НА ДОРАБОТКУ"
─────────────────────────────────────────────────────────
→ Что нужно исправить * (обязательно):
    - Чекбоксы с типичными недочётами (мультивыбор):
        ☐ Добавьте фотографии курса
        ☐ Уточните описание и программу
        ☐ Укажите точную цену
        ☐ Добавьте информацию о преподавателях
        ☐ Уточните формат занятий (онлайн/офлайн)
        ☐ Другое
    - Текстовое поле "Что именно нужно исправить" * (обязательно):
        placeholder: "Опишите конкретные правки..."
        max 1000 символов

→ Кнопка "Отправить на доработку"

─────────────────────────────────────────────────────────
ПОСЛЕ ПОДТВЕРЖДЕНИЯ
─────────────────────────────────────────────────────────
→ Item.moderation_status = revision_required
→ Item.moderation_comment = {текст с перечислением выбранных пунктов + комментарий}
→ Item.item_isvisible = false
→ Item.moderated_by = admin_id
→ Item.moderated_at = now()
→ Toast: "Айтем отправлен на доработку. Продавец уведомлён."
→ Возврат к /admin/moderation/items

Продавец видит в /seller/items:
    Бейдж: [Нужна доработка]
    Текст: "Исправьте следующее и повторно отправьте на проверку:"
    {список пунктов}
    {комментарий верификатора}
    Кнопка: "Исправить и отправить повторно"
```

---

### UC-05 — Альтернативные потоки

**5a. Ни один чекбокс не выбран и текст не заполнен:**
```
UI-реакция:
→ Блок чекбоксов: красная обводка
→ Под блоком: "Выберите хотя бы один пункт или опишите, что нужно исправить"
```

---

### UC-06: Модерация отзывов (жалоба от продавца)

**Актор:** Верификатор или Root
**Предусловие:** Продавец оспорил отзыв через свой кабинет (Spec 14), отзыв переведён в статус `pending_moderation`
**Триггер:** Admin переходит на /admin/moderation/reviews

**Контекст:** Отзывы публикуются немедленно (статус `active` по умолчанию). Если продавец считает отзыв нарушающим правила — он подаёт жалобу. После жалобы отзыв не скрывается автоматически — он остаётся видимым, но попадает в очередь модерации.

**Полный поток:**

```
[Точка входа]
→ Admin в навигации нажимает "Модерация" → вкладка "Отзывы"
→ Открывается /admin/moderation/reviews
→ Список отзывов на рассмотрении:

─────────────────────────────────────────────────────────
СПИСОК ОЧЕРЕДИ: ОТЗЫВЫ
─────────────────────────────────────────────────────────
Каждая строка:
    - Айтем, к которому относится отзыв
    - Имя покупателя (или "Аноним")
    - Рейтинг (1–5 звёзд)
    - Дата отзыва / дата жалобы
    - Начало текста отзыва
    - Кнопка "Рассмотреть"

─────────────────────────────────────────────────────────
ДЕТАЛЬНАЯ СТРАНИЦА: /admin/moderation/reviews/[review_id]
─────────────────────────────────────────────────────────
→ Блок "Отзыв":
    - Полный текст отзыва
    - Рейтинг
    - Дата публикации
    - Покупатель (имя/аноним, кол-во отзывов)

→ Блок "Жалоба продавца":
    - Название школы
    - Причина жалобы (что выбрал продавец): {шаблон}
    - Комментарий продавца к жалобе

→ Блок "Правила платформы" (краткая справка: что является нарушением)

→ Внутренний комментарий (admin only) — опциональное поле:
    placeholder: "Заметки для внутреннего использования (не видны продавцу и покупателю)"
    max 500 символов

→ Панель решения:
    [✅ Вернуть в опубликованные]     ← отзыв остаётся видимым
    [❌ Удалить отзыв]                ← нарушение подтверждено

─────────────────────────────────────────────────────────
ДЕЙСТВИЕ А: ВЕРНУТЬ В ОПУБЛИКОВАННЫЕ
─────────────────────────────────────────────────────────
→ Admin нажимает "✅ Вернуть в опубликованные"
→ Диалог: "Оставить отзыв? Он останется опубликованным. Жалоба будет закрыта."
→ [Отмена] [Подтвердить]
→ Review.moderation_status = active
→ Review.moderation_note = {внутренний комментарий, если заполнен}
→ Toast: "Отзыв оставлен. Жалоба закрыта."
→ (Фоново) Продавцу: "Ваша жалоба на отзыв рассмотрена. Отзыв остаётся на платформе."

─────────────────────────────────────────────────────────
ДЕЙСТВИЕ Б: УДАЛИТЬ ОТЗЫВ
─────────────────────────────────────────────────────────
→ Admin нажимает "❌ Удалить отзыв"
→ Модал:
  "Удалить отзыв?
  Укажите причину удаления (видна только продавцу и покупателю):
  [текстовое поле, обязательное, max 500 символов]"
  [Отмена] [Удалить]
→ Review.moderation_status = rejected
→ Review.moderation_note = {внутренний комментарий}
→ Review.rejection_reason = {причина для участников}
→ Review скрывается из публичного каталога
→ Toast: "Отзыв удалён. Стороны уведомлены."
→ (Фоново) Продавцу: "Ваша жалоба подтверждена. Отзыв удалён."
→ (Фоново) Покупателю: "Ваш отзыв был удалён администрацией. Причина: {rejection_reason}"
```

---

### UC-06 — Альтернативные потоки

**6a. Причина удаления не заполнена:**
```
UI-реакция:
→ Поле: красная обводка + ⚠
→ Под полем: "Укажите причину удаления для покупателя и продавца"
```

**6b. Отзыв уже промодерирован другим верификатором (race condition):**
```
Триггер: PATCH возвращает 409 REVIEW_ALREADY_MODERATED

UI-реакция:
→ Toast (жёлтый): "Этот отзыв уже рассмотрен."
→ Кнопки блокируются
```

---

### UC-07: Управление аккаунтами пользователей

**Актор:** Root
**Предусловие:** Root авторизован
**Триггер:** Root переходит в /admin/users или /admin/users/sellers

**Полный поток — просмотр и поиск:**

```
[Точка входа]
→ Root в навигации нажимает "Пользователи" → /admin/users/sellers
→ Таблица продавцов:
    - Название организации
    - Тип (school_offline / online_school / individual_contributor)
    - Email / Телефон
    - Статус аккаунта (бейдж: [Активен] / [На проверке] / [Заблокирован])
    - Дата регистрации
    - Кол-во активных айтемов
    - Действия: "Подробнее" / "Заблокировать" / "Разблокировать"

→ Поиск: по email, телефону, названию организации
→ Фильтр: по статусу (active / under_review / blocked)
```

**Поток — заблокировать продавца:**

```
→ Root нажимает "Заблокировать" у нужного продавца
→ Модал:
  "Заблокировать аккаунт {org_name}?
  Причина блокировки (видна продавцу):
  [textarea, опционально, max 500 символов]
  Продавец получит уведомление об блокировке."
  [Отмена] [Заблокировать]

→ Account.account_status = blocked
→ Все айтемы продавца скрываются из каталога (принудительно)
→ Продавец при следующей попытке войти видит:
  Страница /seller/blocked:
  "Ваш аккаунт заблокирован.
  Причина: {причина если указана}
  Если вы считаете это ошибкой, напишите нам: support@qadam.uz"
→ Toast: "Аккаунт заблокирован."
```

**Поток — разблокировать продавца:**

```
→ Root нажимает "Разблокировать"
→ Диалог: "Разблокировать аккаунт {org_name}? Все его айтемы (в статусе active) вернутся в каталог."
→ [Отмена] [Разблокировать]
→ Account.account_status = active
→ Айтемы восстанавливаются в каталоге если были active до блокировки
→ Toast: "Аккаунт разблокирован."
```

**Поток — отправить на проверку:**

```
→ Root нажимает "Отправить на проверку"
→ Account.account_status = under_review
→ Продавец видит в /seller баннер:
  "Ваш аккаунт находится на проверке. Создание и редактирование курсов временно недоступно.
  Обратитесь в поддержку: support@qadam.uz"
→ Айтемы продавца скрываются из каталога на время проверки
```

---

### UC-07 — Альтернативные потоки

**7a. Роль Аналитик или Маркетолог пытается заблокировать пользователя:**
```
UI-реакция:
→ Кнопки "Заблокировать" / "Разблокировать" не отображаются (скрыты по роли)
→ Через API: 403 { error: 'INSUFFICIENT_ROLE', message: 'Управление блокировками доступно только Root.' }
```

**7b. Верификатор переходит в /admin/users:**
```
UI-реакция:
→ Раздел "Пользователи" не отображается в навигации для роли verifier
→ 403 при прямом переходе
```

---

## 4. Бизнес-правила и валидации

### Правила модерации айтемов

| Правило | Описание |
|---------|---------|
| Очередь только pending | В очереди модерации отображаются ТОЛЬКО айтемы со статусом `pending`. Черновики, архивные — не попадают. |
| FIFO-сортировка | По умолчанию: старые айтемы (по дате отправки) — первые. |
| Комментарий обязателен | При `rejected` и `revision_required` — текстовый комментарий обязателен. При `approve` — комментарий сбрасывается. |
| Повторная модерация | После `rejected`/`revision_required`: продавец правит → снова `pending` → возвращается в очередь с пометкой "Повторно". |
| Защита от race condition | Первый ответивший верификатор "захватывает" айтем. Второй получает 409. |
| Критичный инвариант каталога | В публичный каталог попадают ТОЛЬКО: `moderation_status = active` AND `item_isvisible = true` AND `seller.account_status = active`. |

### Правила модерации отзывов

| Правило | Описание |
|---------|---------|
| Отзывы видны сразу | По умолчанию `review_status = active`. Жалоба продавца НЕ скрывает отзыв автоматически — он остаётся видимым. |
| Одна активная жалоба | На один отзыв — только одна активная жалоба. Повторная жалоба до рассмотрения первой — заблокирована. |
| Внутренний комментарий | Заметки верификатора видны ТОЛЬКО администраторам. Не попадают в публичный API. |
| Срок рассмотрения | TBD (не определён на MVP). |

### Правила ролевой модели

| Правило | Описание |
|---------|---------|
| Root уникален | Роль `root` не создаётся через UI. Только seed при деплое или прямая правка БД. |
| Root неудаляем через UI | Root-аккаунт нельзя заблокировать или удалить через интерфейс. |
| Смена роли — только Root | Только Root может изменить роль другого admin. |
| Блокировка продавца — только Root | Верификатор, Аналитик, Маркетолог не могут блокировать пользователей. |

### Таблица валидаций полей

| Поле | Правило | Ошибка пользователю |
|------|---------|-------------------|
| Email нового admin | RFC 5322, уникальный | "Этот email уже используется другим аккаунтом." |
| Имя / Фамилия admin | 2–50 символов | "Имя: от 2 до 50 символов" |
| Комментарий модерации (обязательный) | 1–1000 символов | "Укажите причину отклонения/доработки (до 1000 символов)" |
| Внутренняя заметка верификатора | 0–500 символов | "Максимум 500 символов" |
| Причина удаления отзыва | 1–500 символов | "Укажите причину удаления (до 500 символов)" |
| Причина блокировки пользователя | 0–500 символов (опционально) | — |

---

## 5. Модель данных

> Используются существующие сущности: Account, Seller, Item, Review. Новые: AdminProfile, ReviewComplaint.

### AdminProfile (профиль администратора)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| admin_id | UUID | PK |
| account_id | UUID FK | → Account (account_type = ADMIN), unique |
| admin_role | AdminRole | root / verifier / analyst / marketer |
| created_by | UUID FK | → Account (кто создал, nullable для seed root) |
| created_at | DateTime | |
| updated_at | DateTime | |

### Item (расширение существующей сущности)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| item_id | UUID | PK (существующий) |
| seller_id | UUID FK | → Seller (существующий) |
| item_name | string | (существующий) |
| item_slug | string | (существующий) |
| moderation_status | ItemStatus | draft/pending/active/rejected/revision_required/archived |
| item_isvisible | boolean | default: true. Управляется продавцом. |
| moderation_comment | text? | Комментарий верификатора — виден продавцу |
| moderated_by | UUID FK? | → Account (admin), nullable |
| moderated_at | DateTime? | Дата последнего решения по модерации |

### Review (расширение существующей сущности, детали в Spec 14)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| review_id | UUID | PK |
| review_status | ReviewStatus | active / pending_moderation / rejected |
| moderation_note | text? | Внутренняя заметка верификатора (admin only) |
| rejection_reason | text? | Причина удаления (видна продавцу и покупателю) |
| moderated_by | UUID FK? | → Account (admin) |
| moderated_at | DateTime? | |

### ReviewComplaint (жалоба продавца на отзыв)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| complaint_id | UUID | PK |
| review_id | UUID FK | → Review |
| seller_id | UUID FK | → Seller (жалующийся) |
| complaint_reason | ComplaintReason | spam / fake / offensive / other |
| complaint_comment | text? | Пояснение от продавца (max 500 символов) |
| complaint_status | ComplaintStatus | open / resolved |
| created_at | DateTime | |
| resolved_at | DateTime? | |

---

## 6. Технические контракты

### 6.1 Prisma Schema (добавления)

```prisma
enum AdminRole {
  root
  verifier
  analyst
  marketer
}

enum ItemStatus {
  draft
  pending
  active
  rejected
  revision_required
  archived
}

enum ReviewStatus {
  active
  pending_moderation
  rejected
}

enum ComplaintReason {
  spam
  fake
  offensive
  other
}

enum ComplaintStatus {
  open
  resolved
}

model AdminProfile {
  admin_id    String    @id @default(uuid())
  account_id  String    @unique
  admin_role  AdminRole
  created_by  String?   // nullable для seed root
  created_at  DateTime  @default(now())
  updated_at  DateTime  @updatedAt

  account     Account   @relation(fields: [account_id], references: [account_id])
}

model Item {
  item_id            String     @id @default(uuid())
  seller_id          String
  item_name          String     @db.VarChar(200)
  item_slug          String     @unique
  moderation_status  ItemStatus @default(draft)
  item_isvisible     Boolean    @default(true)
  moderation_comment String?    @db.Text
  moderated_by       String?
  moderated_at       DateTime?
  created_at         DateTime   @default(now())
  updated_at         DateTime   @updatedAt

  seller        Seller   @relation(fields: [seller_id], references: [seller_id])
  moderator     Account? @relation("ItemModerator", fields: [moderated_by], references: [account_id])
  reviews       Review[]
  leads         Lead[]
}

// Расширения Review (подробная схема — Spec 14):
// review_status, moderation_note, rejection_reason, moderated_by, moderated_at

model ReviewComplaint {
  complaint_id     String          @id @default(uuid())
  review_id        String
  seller_id        String
  complaint_reason ComplaintReason
  complaint_comment String?        @db.VarChar(500)
  complaint_status ComplaintStatus @default(open)
  created_at       DateTime        @default(now())
  resolved_at      DateTime?

  seller Seller @relation(fields: [seller_id], references: [seller_id])

  @@unique([review_id, seller_id])  // одна жалоба от продавца на один отзыв
}
```

### 6.2 TypeScript DTO

```typescript
// ─── Создание администратора ───────────────────────────────────────────────

export class CreateAdminDto {
  @IsString() @MinLength(2) @MaxLength(50)
  first_name: string

  @IsString() @MinLength(2) @MaxLength(50)
  last_name: string

  @IsEmail()
  email: string

  @IsEnum(['verifier', 'analyst', 'marketer'])
  // root нельзя создать через UI
  admin_role: 'verifier' | 'analyst' | 'marketer'
}

// ─── Решение по модерации айтема ──────────────────────────────────────────

export class ModerateItemDto {
  @IsEnum(['active', 'rejected', 'revision_required'])
  decision: 'active' | 'rejected' | 'revision_required'

  @IsOptional() @IsString() @MinLength(1) @MaxLength(1000)
  // обязателен при rejected и revision_required
  moderation_comment?: string
}

// ─── Решение по модерации отзыва ──────────────────────────────────────────

export class ModerateReviewDto {
  @IsEnum(['active', 'rejected'])
  decision: 'active' | 'rejected'

  @IsOptional() @IsString() @MaxLength(500)
  moderation_note?: string  // внутренняя заметка

  @IsOptional() @IsString() @MinLength(1) @MaxLength(500)
  // обязателен при decision = rejected
  rejection_reason?: string
}

// ─── Управление статусом аккаунта пользователя ───────────────────────────

export class UpdateAccountStatusDto {
  @IsEnum(['active', 'under_review', 'blocked'])
  status: 'active' | 'under_review' | 'blocked'

  @IsOptional() @IsString() @MaxLength(500)
  reason?: string
}

// ─── Response ─────────────────────────────────────────────────────────────

export interface ModerationItemResponse {
  item_id: string
  item_name: string
  item_slug: string
  seller: {
    seller_id: string
    org_name: string
    account_status: AccountStatus
  }
  moderation_status: ItemStatus
  moderation_comment: string | null
  submitted_at: string    // дата отправки на текущую модерацию
  is_resubmission: boolean
  // полные данные айтема для просмотра:
  description: string
  short_desc: string
  prices: ItemPriceDto[]
  media: ItemMediaDto[]
  performers: PerformerPublicDto[]
}

export interface ModerationQueueResponse {
  items: ModerationItemResponse[]
  total: number
  pending_count: number
}

export interface AdminProfileResponse {
  admin_id: string
  account_id: string
  first_name: string
  last_name: string
  email: string
  admin_role: AdminRole
  created_at: string
}

export interface ReviewModerationResponse {
  review_id: string
  review_text: string
  rating: number
  buyer_name: string | null
  item_name: string
  seller_name: string
  review_status: ReviewStatus
  complaint: {
    complaint_id: string
    complaint_reason: ComplaintReason
    complaint_comment: string | null
    created_at: string
  }
  moderation_note: string | null
}
```

### 6.3 API Endpoints

```
────────────────────────────────────────────────────────────────
ADMIN: УПРАВЛЕНИЕ КОМАНДОЙ (только Root)
────────────────────────────────────────────────────────────────

GET /api/admin/team
Auth: Bearer (admin: root)
→ 200: { admins: AdminProfileResponse[], total: number }
→ 403: { error: 'INSUFFICIENT_ROLE' }

POST /api/admin/team
Auth: Bearer (admin: root)
Body: CreateAdminDto
→ 201: AdminProfileResponse
→ 409: { error: 'EMAIL_TAKEN', message: 'Этот email уже используется другим аккаунтом.' }
→ 400: { error: 'CANNOT_CREATE_ROOT', message: 'Роль Root нельзя создать через интерфейс.' }
→ 422: { errors: ValidationError[] }

PATCH /api/admin/team/:admin_id/role
Auth: Bearer (admin: root)
Body: { admin_role: AdminRole }
→ 200: AdminProfileResponse
→ 400: { error: 'CANNOT_CHANGE_ROOT_ROLE', message: 'Роль Root нельзя изменить.' }
→ 403: { error: 'INSUFFICIENT_ROLE' }
→ 404: { error: 'ADMIN_NOT_FOUND' }

DELETE /api/admin/team/:admin_id
Auth: Bearer (admin: root)
→ 204
→ 400: { error: 'CANNOT_DELETE_ROOT', message: 'Root-аккаунт нельзя удалить через интерфейс.' }
→ 403: { error: 'INSUFFICIENT_ROLE' }

POST /api/admin/team/:admin_id/resend-invite
Auth: Bearer (admin: root)
→ 200: { message: 'Приглашение отправлено.' }

────────────────────────────────────────────────────────────────
ADMIN: МОДЕРАЦИЯ АЙТЕМОВ (Root + Верификатор)
────────────────────────────────────────────────────────────────

GET /api/admin/moderation/items
Auth: Bearer (admin: root | verifier)
Query: ?status=pending&sort=asc&type=all|primary|resubmission&page=1&limit=20
→ 200: ModerationQueueResponse
→ 403: { error: 'INSUFFICIENT_ROLE' }

GET /api/admin/moderation/items/:item_id
Auth: Bearer (admin: root | verifier)
→ 200: ModerationItemResponse
→ 403: { error: 'INSUFFICIENT_ROLE' }
→ 404: { error: 'ITEM_NOT_FOUND' }

PATCH /api/admin/moderation/items/:item_id
Auth: Bearer (admin: root | verifier)
Body: ModerateItemDto
→ 200: { item_id: string, moderation_status: ItemStatus, moderated_at: string }
→ 400: { error: 'COMMENT_REQUIRED', message: 'Укажите причину отклонения для продавца.' }
→ 403: { error: 'INSUFFICIENT_ROLE' }
→ 404: { error: 'ITEM_NOT_FOUND' }
→ 409: { error: 'ITEM_ALREADY_MODERATED', message: 'Этот айтем уже промодерирован.', current_status: ItemStatus }

────────────────────────────────────────────────────────────────
ADMIN: МОДЕРАЦИЯ ОТЗЫВОВ (Root + Верификатор)
────────────────────────────────────────────────────────────────

GET /api/admin/moderation/reviews
Auth: Bearer (admin: root | verifier)
Query: ?page=1&limit=20
→ 200: { reviews: ReviewModerationResponse[], total: number }
→ 403: { error: 'INSUFFICIENT_ROLE' }

GET /api/admin/moderation/reviews/:review_id
Auth: Bearer (admin: root | verifier)
→ 200: ReviewModerationResponse
→ 403: { error: 'INSUFFICIENT_ROLE' }
→ 404: { error: 'REVIEW_NOT_FOUND' }

PATCH /api/admin/moderation/reviews/:review_id
Auth: Bearer (admin: root | verifier)
Body: ModerateReviewDto
→ 200: { review_id: string, review_status: ReviewStatus }
→ 400: { error: 'REJECTION_REASON_REQUIRED', message: 'Укажите причину удаления отзыва.' }
→ 403: { error: 'INSUFFICIENT_ROLE' }
→ 409: { error: 'REVIEW_ALREADY_MODERATED' }

────────────────────────────────────────────────────────────────
ADMIN: УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ (Root: блокировка; Analyst/Marketer: просмотр)
────────────────────────────────────────────────────────────────

GET /api/admin/users/sellers
Auth: Bearer (admin: root | analyst | marketer)
Query: ?status=active|under_review|blocked&search=term&page=1&limit=50
→ 200: { sellers: SellerAdminView[], total: number }
→ 403: { error: 'INSUFFICIENT_ROLE' }

GET /api/admin/users/sellers/:seller_id
Auth: Bearer (admin: root | analyst | marketer)
→ 200: SellerAdminView (полный профиль + история статусов + список айтемов)
→ 403: { error: 'INSUFFICIENT_ROLE' }
→ 404: { error: 'SELLER_NOT_FOUND' }

PATCH /api/admin/users/sellers/:seller_id/status
Auth: Bearer (admin: root only)
Body: UpdateAccountStatusDto
→ 200: { seller_id: string, account_status: AccountStatus }
→ 400: { error: 'INVALID_STATUS_TRANSITION', message: string }
→ 403: { error: 'INSUFFICIENT_ROLE', message: 'Управление блокировками доступно только Root.' }
→ 404: { error: 'SELLER_NOT_FOUND' }

GET /api/admin/users/buyers
Auth: Bearer (admin: root | analyst | marketer)
Query: ?page=1&limit=50&search=term
→ 200: { buyers: BuyerAdminView[], total: number }

────────────────────────────────────────────────────────────────
ПУБЛИЧНЫЙ КАТАЛОГ (критичный инвариант — проверка на бэкенде)
────────────────────────────────────────────────────────────────

// Любой GET /api/items или /api/items?... ОБЯЗАН включать фильтр:
// WHERE moderation_status = 'active'
//   AND item_isvisible = true
//   AND seller.account_status = 'active'
// Это не опция — это жёсткий инвариант, нарушение = критичный баг.
```

---

## 7. Edge Cases и обработка ошибок

| Сценарий | Поведение |
|----------|----------|
| Верификатор одновременно открыл один айтем с коллегой | Первый сохранивший решение получает 200. Второй — 409 ITEM_ALREADY_MODERATED. |
| Продавец отозвал айтем с модерации пока верификатор его смотрит | PATCH от верификатора возвращает 409 с сообщением: "Продавец отозвал айтем с проверки." |
| Продавец заблокирован: что с его pending айтемами? | Pending айтемы остаются в очереди. После разблокировки — продавец сам решает что делать. |
| Root удаляет сам себя | 400: CANNOT_DELETE_ROOT. Защита на уровне бэкенда. |
| Аналитик пытается открыть /admin/moderation через прямой URL | 403 INSUFFICIENT_ROLE. Навигация скрыта. |
| Отзыв удалён, но продавец повторно подаёт жалобу на него | 400: REVIEW_ALREADY_REJECTED. |
| Продавец заблокирован — его айтемы в статусе active | Айтемы НЕ меняют moderation_status, но не появляются в каталоге (фильтр по seller.account_status). При разблокировке — мгновенно возвращаются. |
| Верификатор пытается блокировать пользователя | 403 INSUFFICIENT_ROLE. Нет UI-кнопки для этой роли. |
| Item со статусом draft попал в каталог (защита от бага) | Backend: жёсткий WHERE-фильтр. Middleware для catalog-endpoints обязательно добавляет `moderation_status = 'active'`. |
| Admin входит через /login с обычными credentials | Стандартный /login работает для всех account_type. После логина: редирект на /admin вместо /seller. |

---

## 8. TBD / Сознательно опущено

| Тема | Статус | Примечание |
|------|--------|-----------|
| Ролевая модель в MVP для Login | TBD | При входе через /login: если account_type = ADMIN → редирект /admin. Логика редиректа не специфицирована отдельно. |
| Уведомления продавцам о решениях по модерации | Частично | На MVP: уведомление через кабинет (бейдж + комментарий). Email/Telegram-уведомление — v1.0. |
| Аудит-лог действий администраторов | Исключено из MVP | Все модерационные действия должны логироваться. Реализация audit_log — v1.0. |
| SLA на модерацию | TBD | Время ответа по жалобе на отзыв не определено. |
| Пагинация очереди модерации | Реализовать | limit=20 по умолчанию. Важно для масштаба (800+ продавцов → тысячи айтемов). |
| Bulk-действия (одобрить/отклонить несколько сразу) | Исключено из MVP | Только единичные действия на MVP. Bulk — v1.0. |
| Автоматические правила модерации (ML) | Вне скоупа | Не планируется в MVP. |
| Admin 2FA | Исключено из MVP | Критично для безопасности. Добавить в v1.0. |
| "Захват" айтема (assign to me) | TBD | Нужен ли механизм назначения айтема на конкретного верификатора? На MVP — нет. |
| История решений по конкретному айтему | TBD | Показывать ли верификатору историю предыдущих решений? Полезно при повторной модерации. |
| Причины блокировки продавца — шаблоны | TBD | Свободный текст на MVP. Шаблоны причин — v1.0. |
| Модерация Buyer-аккаунтов | Исключено из MVP | На MVP байеры не модерируются. |

---

## Зависимости

| Модуль | Связь |
|--------|-------|
| **Spec 01** (Seller Onboarding) | Account.account_status изменяется в UC-07 |
| **Spec 02** (Item Management) | Item.moderation_status и Item.item_isvisible — основные сущности UC-02–UC-05 |
| **Spec 03** (Staff Management) | PerformerProfile видима только при active seller (фильтр в публичном API) |
| **Spec 05** (Catalog) | Каталог ОБЯЗАН фильтровать: active + isvisible=true + seller.active |
| **Spec 14** (Reviews) | ReviewComplaint.review_id → Review.review_id. Жалоба продавца — триггер UC-06. |
| **Spec 16** (Reference Data) | Marketer-роль управляет subjects/locations через /admin/reference/* |
