# MVP Spec 13 — Buyer Personal Cabinet

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

- Статус документа: 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: B (Demand)
> Status: Draft v1
> Sync note, 28 Mar 2026:
> - live API prefix is `/api/v1/`, not `/api/`;
> - текущий buyer cabinet API ограничен `GET/POST/PATCH /api/v1/me/profile`, `GET /api/v1/me/leads`, `GET /api/v1/me/reviews`;
> - `/api/me/overview`, avatar upload и change-password endpoints пока не являются текущим production contract.

---

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

Личный кабинет покупателя (`/me/*`) — это персональная зона пользователя на платформе Qadam. Здесь покупатель управляет своим профилем, отслеживает отправленные заявки (лиды) и просматривает написанные отзывы.

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

**Что не входит в этот модуль:**
- Написание нового отзыва (инициируется со страницы курса) → Spec 14
- Подача заявки (лида) → Spec 05 (Lead Management)
- Авторизация и регистрация покупателя → Spec 07 (Auth)
- История платежей → Spec v1.5 (Payments)
- Уведомления → Spec 10

---

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

| Роль | Действия в этом модуле |
|------|----------------------|
| **Покупатель (авторизованный)** | Просмотр дашборда, просмотр лидов, редактирование профиля, просмотр отзывов |
| **Гость (неавторизованный)** | Перенаправляется на /login при попытке доступа к /me/* |

---

## 3. Use Cases

---

### UC-01: Покупатель открывает /me (Overview — дашборд)

**Актор:** Покупатель (авторизованный)
**Предусловие:** Пользователь авторизован как BUYER
**Триггер:** Нажимает "Мой кабинет" в хедере или переходит на /me напрямую

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

```
[Точка входа]
→ Пользователь нажимает аватар / "Мой кабинет" в хедере
→ Открывается /me

─────────────────────────────────────────────────────────
НАВИГАЦИЯ КАБИНЕТА (левая панель / top tabs)
─────────────────────────────────────────────────────────
→ Пункты меню:
    [Обзор]       → /me         (активна)
    [Профиль]     → /me/profile
    [Мои заявки]  → /me/leads
    [Мои отзывы]  → /me/reviews

─────────────────────────────────────────────────────────
РАЗДЕЛ — Приветствие
─────────────────────────────────────────────────────────
→ "Привет, {first_name}!" (берётся из BuyerProfile.first_name)
→ Если first_name не заполнен: "Привет!" (без имени)

─────────────────────────────────────────────────────────
РАЗДЕЛ — Метрики (Stats Cards)
─────────────────────────────────────────────────────────
→ Три карточки с числами:

    ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
    │  Всего заявок   │  │  Активных курсов│  │  Отзывов        │
    │       12        │  │       3         │  │       5         │
    └─────────────────┘  └─────────────────┘  └─────────────────┘

    "Всего заявок"    = COUNT(Lead) где buyer_id = текущий
    "Активных курсов" = COUNT(Lead) где lead_status IN (enrolled, attended, purchased)
    "Отзывов"         = COUNT(Review) где buyer_id = текущий AND status != rejected

─────────────────────────────────────────────────────────
РАЗДЕЛ — Последние заявки (Recent Leads)
─────────────────────────────────────────────────────────
→ Заголовок: "Последние заявки" + ссылка "Все заявки →"
→ Список последних 3 лидов:
    Каждая запись:
    - Обложка курса (cover_url) или заглушка
    - Название курса (ссылка на /item/[slug])
    - Название школы (ссылка на /sellers/[id])
    - Статус лида: бейдж [Новая] / [В обработке] / [Зачислен] / [Отклонён]
    - Дата подачи заявки
→ Статусы цветовые:
    pending → серый
    processing → жёлтый
    enrolled / attended / purchased → зелёный
    rejected → красный
→ Если лидов нет:
    "Вы ещё не оставляли заявок."
    Кнопка "Найти курс" → /catalog

─────────────────────────────────────────────────────────
РАЗДЕЛ — Последние отзывы (Recent Reviews)
─────────────────────────────────────────────────────────
→ Заголовок: "Мои отзывы" + ссылка "Все отзывы →"
→ Последние 2 отзыва:
    - Название курса + звёзды рейтинга + дата
    - Статус: [На проверке] / [Опубликован] / [Удалён]
→ Если отзывов нет — блок не отображается
```

---

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

**1a. Ошибка загрузки метрик:**
```
UI-реакция:
→ В карточке метрики вместо числа: "—"
→ Toast не показывается (тихий fallback)
```

**1b. Долгая загрузка (> 1 сек):**
```
UI-реакция:
→ Карточки метрик и список лидов показывают skeleton-loader
→ Spinner не используется
```

---

### UC-02: Покупатель просматривает список заявок (/me/leads)

**Актор:** Покупатель (авторизованный)
**Предусловие:** Пользователь авторизован
**Триггер:** Нажимает "Мои заявки" в навигации кабинета

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

```
→ Открывается /me/leads

─────────────────────────────────────────────────────────
ФИЛЬТРЫ
─────────────────────────────────────────────────────────
→ Фильтр по статусу (tabs или dropdown):
    [Все] [Новые] [В обработке] [Зачислен] [Отклонён]
→ По умолчанию: "Все"

─────────────────────────────────────────────────────────
СПИСОК ЗАЯВОК
─────────────────────────────────────────────────────────
→ Каждая заявка — карточка:
    - Обложка курса (cover_url) или серый placeholder
    - Название курса (кликабельная ссылка → /item/[slug])
    - Продавец: логотип + название школы (кликабельная ссылка → /sellers/[id])
    - Дата подачи заявки (напр. "14 февраля 2026")
    - Статус: цветной бейдж
    - Кнопка "Подробнее" → открывает detail panel (UC-03)

→ Пагинация: 10 заявок на страницу
→ Сортировка: по дате DESC (newest first), нет возможности изменить

─────────────────────────────────────────────────────────
ПУСТОЕ СОСТОЯНИЕ
─────────────────────────────────────────────────────────
→ Иллюстрация
→ "У вас пока нет заявок"
→ Кнопка "Найти курс" → /catalog
```

---

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

**2a. Ошибка загрузки списка заявок:**
```
UI-реакция:
→ Вместо списка: "Не удалось загрузить заявки. Попробуйте снова."
→ Кнопка "Повторить"
```

**2b. Фильтр выбран, но заявок с таким статусом нет:**
```
UI-реакция:
→ Пустое состояние: "Нет заявок со статусом '{статус}'"
→ Ссылка "Показать все заявки"
```

---

### UC-03: Покупатель просматривает детали заявки

**Актор:** Покупатель (авторизованный)
**Предусловие:** Заявка существует и принадлежит этому покупателю
**Триггер:** Нажимает "Подробнее" на карточке заявки в /me/leads

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

```
→ Открывается боковая панель (drawer) или отдельная страница
  с деталями заявки

─────────────────────────────────────────────────────────
СОДЕРЖИМОЕ ПАНЕЛИ ДЕТАЛЕЙ
─────────────────────────────────────────────────────────
→ Название курса (ссылка на /item/[slug])
→ Школа (ссылка на /sellers/[id])
→ Статус заявки (бейдж + расшифровка):
    pending     → "Заявка принята. Ожидайте контакта от школы."
    processing  → "Школа рассматривает вашу заявку."
    enrolled    → "Поздравляем! Вы зачислены на курс."
    attended    → "Вы посещаете курс."
    purchased   → "Курс оплачен."
    rejected    → "К сожалению, школа отклонила вашу заявку."

→ Дата подачи заявки
→ Контакты продавца (видны всегда после принятия заявки):
    Телефон: +998 90 123-45-67 (кликабельный)
    Email: school@example.com (кликабельный)
    Адрес (если school_offline и display_publicly = true)

→ Комментарий покупателя (если был при подаче заявки)
→ Примечание от школы (lead.seller_note, если заполнено):
    Блок с фоном: "Сообщение от школы:"
    Текст примечания

─────────────────────────────────────────────────────────
CTA — НАПИСАТЬ ОТЗЫВ
─────────────────────────────────────────────────────────
→ Кнопка "Оставить отзыв" отображается если:
    - lead_status IN (enrolled, attended, purchased)
    - Покупатель ещё не написал отзыв на этот айтем (review отсутствует)
→ При нажатии: переход на /item/[slug]#review-form
  (якорная ссылка на форму отзыва на странице курса)
→ Если отзыв уже написан: кнопка заменяется на "Отзыв написан ✓"
  (ссылка на /me/reviews)
```

---

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

**3a. Попытка открыть чужую заявку (перебор lead_id):**
```
API-реакция:
→ GET /api/v1/me/leads/:lead_id → 403 FORBIDDEN
UI-реакция:
→ Панель не открывается
→ Toast (красный): "Нет доступа к этой заявке."
```

**3b. Заявка удалена или более не существует:**
```
API-реакция:
→ 404 LEAD_NOT_FOUND
UI-реакция:
→ Toast (красный): "Заявка не найдена."
→ Список обновляется
```

---

### UC-04: Покупатель редактирует профиль (/me/profile)

**Актор:** Покупатель (авторизованный)
**Предусловие:** Пользователь авторизован
**Триггер:** Нажимает "Профиль" в навигации кабинета

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

```
→ Открывается /me/profile

─────────────────────────────────────────────────────────
ФОРМА ПРОФИЛЯ
─────────────────────────────────────────────────────────
→ Аватар:
    Круглое фото (avatar_url) или заглушка-инициалы
    Кнопка "Изменить фото" (загрузка файла)

→ Поля формы (предзаполнены текущими данными):
    Имя *               (2–50 символов)
    Фамилия *           (2–50 символов)
    Телефон             (+998XXXXXXXXX, необязательно)
    Email               (read-only — отображается, но не редактируется)
    Дата рождения       (необязательно, date picker)
    Город               (необязательно, free text или select)

→ Кнопка "Сохранить изменения"

─────────────────────────────────────────────────────────
СЕКЦИЯ — СМЕНА ПАРОЛЯ
─────────────────────────────────────────────────────────
→ Отдельный блок ниже формы профиля:
    "Изменить пароль"
    Текущий пароль *
    Новый пароль *       (min 8 символов)
    Подтвердите пароль *
    Кнопка "Изменить пароль"

→ Валидация при потере фокуса (on blur)
→ После сохранения: Toast "Профиль обновлён ✓"
```

---

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

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

**4b. Загрузка аватара — файл > 5 МБ:**
```
UI-реакция:
→ Под полем фото: ⚠ "Файл слишком большой. Максимум 5 МБ."
→ Файл не принимается, аватар не меняется
```

**4c. Загрузка аватара — неверный формат:**
```
UI-реакция:
→ Под полем фото: ⚠ "Поддерживаются форматы JPG, PNG, WebP."
→ Файл не принимается
```

**4d. Неверный текущий пароль:**
```
API-реакция:
→ 400 WRONG_CURRENT_PASSWORD
UI-реакция:
→ Поле "Текущий пароль": красная обводка + ⚠
→ Под полем: "Неверный текущий пароль."
```

**4e. Новый пароль не совпадает с подтверждением:**
```
UI-реакция (client-side, до отправки):
→ Поле "Подтвердите пароль": красная обводка + ⚠
→ Под полем: "Пароли не совпадают."
→ Форма не отправляется
```

**4f. Новый пароль слишком слабый:**
```
API-реакция (или client-side):
→ 400 PASSWORD_TOO_WEAK
UI-реакция:
→ Поле "Новый пароль": красная обводка + ⚠
→ Под полем: "Пароль должен содержать минимум 8 символов."
```

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

---

### UC-05: Покупатель просматривает свои отзывы (/me/reviews)

**Актор:** Покупатель (авторизованный)
**Предусловие:** Пользователь авторизован
**Триггер:** Нажимает "Мои отзывы" в навигации кабинета

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

```
→ Открывается /me/reviews

─────────────────────────────────────────────────────────
СПИСОК ОТЗЫВОВ
─────────────────────────────────────────────────────────
→ Каждый отзыв — карточка:
    - Обложка курса (cover_url) или серый placeholder
    - Название курса (ссылка на /item/[slug])
    - Школа (ссылка на /sellers/[id])
    - Звёзды рейтинга (1–5)
    - Дата написания
    - Текст отзыва
    - Статус отзыва (видит только покупатель):
        pending           → бейдж жёлтый "На проверке"
        published         → бейдж зелёный "Опубликован"
        rejected          → бейдж красный "Удалён"
        pending_moderation→ бейдж жёлтый "На повторной проверке"

    - Если status = rejected:
        Под карточкой серый блок:
        "Ваш отзыв был удалён администратором."

    - Если status = pending_moderation:
        Под карточкой серый блок:
        "Ваш отзыв временно скрыт — поступила жалоба.
         Он будет проверен администратором."

    - Ответ продавца (если seller_reply заполнен и status = published):
        Блок с фоном: "{org_name} отвечает:"
        Текст ответа + дата ответа

─────────────────────────────────────────────────────────
ПУСТОЕ СОСТОЯНИЕ
─────────────────────────────────────────────────────────
→ "Вы ещё не оставляли отзывов."
→ "Оставить отзыв можно на странице курса после подачи заявки."
→ Кнопка "Перейти в каталог" → /catalog
```

---

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

**5a. Ошибка загрузки списка отзывов:**
```
UI-реакция:
→ "Не удалось загрузить отзывы. Попробуйте снова."
→ Кнопка "Повторить"
```

---

### UC-06: Неавторизованный пользователь пытается открыть /me

**Актор:** Гость (неавторизованный)
**Триггер:** Вводит /me/* в адресную строку или переходит по ссылке

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

```
→ Middleware проверяет наличие сессии / JWT токена
→ Токен отсутствует или просрочен

→ Пользователь перенаправляется на /login
→ В URL добавляется параметр return:
    /login?return=/me
    /login?return=/me/leads
    /login?return=/me/reviews
→ После успешного входа:
    Автоматический редирект на исходный URL (из параметра return)
    Если return параметр не задан или невалидный → редирект на /me

─────────────────────────────────────────────────────────
НА СТРАНИЦЕ /login
─────────────────────────────────────────────────────────
→ Сообщение не показывается (тихий редирект, не ошибка)
→ Стандартная форма входа
```

---

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

| Правило | Описание | Ошибка / Поведение |
|---------|----------|--------------------|
| Доступ только для BUYER | /me/* доступно только аккаунтам с account_type = BUYER | 401/редирект на /login |
| Изоляция данных | Покупатель видит только свои лиды и отзывы | 403 при попытке доступа к чужим данным |
| Email — read-only | Email нельзя изменить через /me/profile | Поле отображается как disabled |
| Имя обязательно | first_name и last_name обязательны в профиле | "Это поле обязательно для заполнения" |
| Имя: длина | 2–50 символов | "Имя: от 2 до 50 символов" |
| Телефон: формат | +998XXXXXXXXX если заполнен | "Введите номер в формате +998XXXXXXXXX" |
| Аватар: размер | max 5 МБ | ⚠ "Файл слишком большой. Максимум 5 МБ." |
| Аватар: формат | JPG, PNG, WebP | ⚠ "Поддерживаются форматы JPG, PNG, WebP." |
| Метрика "Активных курсов" | lead_status IN (enrolled, attended, purchased) | |
| Метрика "Отзывов" | status != rejected | Не считаются удалённые |
| Пароль: длина | min 8 символов | "Пароль должен содержать минимум 8 символов." |
| Смена пароля: верификация | Требует ввода текущего пароля | 400 WRONG_CURRENT_PASSWORD |
| Отображение отзывов | Все статусы (кроме rejected) видны покупателю | Rejected отображается с пометкой "Удалён" |
| Кнопка "Оставить отзыв" | Только если lead_status IN (enrolled, attended, purchased) AND нет review | Скрыта если условие не выполнено |

---

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

Этот модуль не создаёт новых сущностей в большинстве случаев. Единственное расширение — BuyerProfile.

### BuyerProfile (профиль покупателя)

| Атрибут | Тип | Описание |
|---------|-----|---------|
| buyer_profile_id | UUID | PK |
| account_id | UUID FK | → Account (unique) |
| first_name | string | 2–50 символов |
| last_name | string | 2–50 символов |
| phone | string? | +998XXXXXXXXX, nullable |
| date_of_birth | Date? | nullable |
| city | string? | nullable, до 100 символов |
| avatar_url | string? | URL в CDN, nullable |
| updated_at | DateTime | |

### Используемые сущности (read-only)

| Сущность | Задействованные атрибуты | Источник |
|----------|--------------------------|----------|
| Account | account_id, email, account_type, account_status | Spec 07 |
| Lead | lead_id, item_id, seller_id, buyer_id, lead_status, seller_note, created_at | Spec 05 |
| Item | item_id, slug, title, cover_url | Spec 02 |
| Seller | seller_id, seller_type | Spec 01 |
| SchoolProfile / OnlineSchoolProfile | org_name, logo_url, phone, email | Spec 01 |
| IndividualContributorProfile | first_name, last_name | Spec 01 |
| Review | review_id, item_id, rating, text, status, seller_reply, seller_reply_at, created_at | Spec 14 |

### Метрики /me (computed, не хранятся в БД)

| Метрика | Формула |
|---------|---------|
| total_leads | COUNT(Lead) WHERE buyer_id = :id |
| active_courses | COUNT(Lead) WHERE buyer_id = :id AND lead_status IN (enrolled, attended, purchased) |
| total_reviews | COUNT(Review) WHERE buyer_id = :id AND status != rejected |

---

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

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

```prisma
// Профиль покупателя определён в Spec 08. Этот модуль использует существующие таблицы:
//   Parent { parent_id, buyer_id, first_name, last_name, phone, email?, avatar_url?, ... }
//   Student { student_id, buyer_id, first_name, last_name, date_of_birth?, class_number?, ... }
// НЕТ единого model BuyerProfile — используем Parent или Student в зависимости от buyer_type.
// email читается из Account (не из Parent/Student), изменение email исключено из MVP.
```

### 6.2 TypeScript DTOs

```typescript
// ─── Профиль покупателя ───────────────────────────────────────────────────

export interface BuyerProfileDto {
  buyer_profile_id: string
  first_name: string
  last_name: string
  phone: string | null
  email: string          // из Account — read-only
  date_of_birth: string | null  // ISO date "YYYY-MM-DD"
  city: string | null
  avatar_url: string | null
}

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

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

  @IsOptional() @Matches(/^\+998\d{9}$/)
  phone?: string

  @IsOptional() @IsDateString()
  date_of_birth?: string

  @IsOptional() @IsString() @MaxLength(100)
  city?: string

  @IsOptional() @IsUrl()
  avatar_url?: string
}

export class ChangeBuyerPasswordDto {
  @IsString()
  current_password: string

  @IsString() @MinLength(8)
  new_password: string

  @IsString()
  confirm_password: string
}

// ─── Дашборд (обзор) ─────────────────────────────────────────────────────

export interface BuyerOverviewDto {
  greeting_name: string | null  // first_name или null
  stats: BuyerStatsDto
  recent_leads: BuyerLeadListItemDto[]   // последние 3
  recent_reviews: BuyerReviewListItemDto[] // последние 2
}

export interface BuyerStatsDto {
  total_leads: number
  active_courses: number
  total_reviews: number
}

// ─── Заявки покупателя ────────────────────────────────────────────────────

export interface BuyerLeadListItemDto {
  lead_id: string
  item_id: string
  item_title: string
  item_slug: string
  item_cover_url: string | null
  seller_id: string
  seller_name: string       // org_name или first_name + last_name
  seller_logo_url: string | null
  lead_status: LeadStatus
  created_at: string        // ISO 8601
}

export interface BuyerLeadDetailDto extends BuyerLeadListItemDto {
  seller_phone: string | null
  seller_email: string | null
  seller_address: string | null  // full_address если display_publicly = true
  buyer_comment: string | null
  seller_note: string | null
  can_review: boolean    // true если eligible и нет review
  review_id: string | null  // если уже есть отзыв
}

// Canonical LeadStatus — см. knowledge-base.md "Canonical Enums"
export type LeadStatus = 'new' | 'contacted' | 'enrolled' | 'attended' | 'no_show' | 'purchased' | 'not_purchased' | 'rejected'

// ─── Отзывы покупателя ────────────────────────────────────────────────────

export interface BuyerReviewListItemDto {
  review_id: string
  item_id: string
  item_title: string
  item_slug: string
  item_cover_url: string | null
  seller_id: string
  seller_name: string
  rating: number
  text: string
  status: ReviewStatus
  created_at: string
  seller_reply: string | null
  seller_reply_at: string | null
}

export type ReviewStatus = 'pending' | 'published' | 'rejected' | 'pending_moderation'
```

### 6.3 API Endpoints

```
────────────────────────────────────────────────────────────────
BUYER CABINET: ОБЗОР
────────────────────────────────────────────────────────────────

GET /api/v1/me/overview
Auth: Bearer (buyer)
→ 200: BuyerOverviewDto
→ 401: { error: 'UNAUTHORIZED' }

────────────────────────────────────────────────────────────────
BUYER CABINET: ПРОФИЛЬ
────────────────────────────────────────────────────────────────

GET /api/v1/me/profile
Auth: Bearer (buyer)
→ 200: BuyerProfileDto
→ 401: { error: 'UNAUTHORIZED' }

PATCH /api/v1/me/profile
Auth: Bearer (buyer)
Body: UpdateBuyerProfileDto
→ 200: BuyerProfileDto
→ 422: { errors: ValidationError[] }

POST /api/v1/me/profile/avatar
Auth: Bearer (buyer)
Body: multipart/form-data { file: File }
→ 200: { avatar_url: string }
→ 400: { error: 'FILE_TOO_LARGE' | 'INVALID_FILE_TYPE', message: string }

POST /api/v1/me/change-password
Auth: Bearer (buyer)
Body: ChangeBuyerPasswordDto
→ 200: { success: true }
→ 400: { error: 'WRONG_CURRENT_PASSWORD' | 'PASSWORDS_DO_NOT_MATCH' | 'PASSWORD_TOO_WEAK', message: string }

────────────────────────────────────────────────────────────────
BUYER CABINET: ЗАЯВКИ
────────────────────────────────────────────────────────────────

GET /api/v1/me/leads
Auth: Bearer (buyer)
Query: ?status=pending&page=1&limit=10
→ 200: { leads: BuyerLeadListItemDto[], total: number, has_more: boolean }
→ 401: { error: 'UNAUTHORIZED' }

GET /api/v1/me/leads/:lead_id
Auth: Bearer (buyer)
→ 200: BuyerLeadDetailDto
→ 403: { error: 'FORBIDDEN' }
→ 404: { error: 'LEAD_NOT_FOUND' }

────────────────────────────────────────────────────────────────
BUYER CABINET: ОТЗЫВЫ
────────────────────────────────────────────────────────────────

GET /api/v1/me/reviews
Auth: Bearer (buyer)
Query: ?page=1&limit=10
→ 200: { reviews: BuyerReviewListItemDto[], total: number, has_more: boolean }
→ 401: { error: 'UNAUTHORIZED' }
```

---

## 7. Edge Cases

| Сценарий | Поведение |
|----------|----------|
| Гость переходит на /me/leads | Редирект на /login?return=/me/leads |
| Продавец типа Seller пытается открыть /me | 403 — /me доступен только BUYER. Seller идёт на /seller |
| BuyerProfile ещё не создан (новый аккаунт) | Создаётся при первом обращении с пустыми полями; first_name и last_name заполняются из данных регистрации |
| Покупатель удалил аккаунт (account_status = deleted) | Токен инвалидируется; при обращении к /api/v1/me/* → 401 |
| lead_id из другого buyer_id в URL | 403 FORBIDDEN — никогда 404 (чтобы не раскрывать существование) |
| avatar_url = broken URL | onError → заглушка-инициалы |
| Параметр return содержит внешний домен | Игнорируется; редирект только на /me после входа |
| Метрики при 0 лидах / 0 отзывов | Числа = 0, не null; отображается "0" |
| Загрузка аватара: файл не изображение (напр. PDF) | 400 INVALID_FILE_TYPE |
| Покупатель пытается изменить email | Поле disabled, PATCH игнорирует email даже если передан |
| Отзыв в статусе pending_moderation | Виден в /me/reviews с пометкой "На повторной проверке"; скрыт с публичных страниц |
| Пустой return параметр в /login?return= | Редирект на /me после входа |

---

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

| Тема | Статус | Примечание |
|------|--------|-----------|
| Избранные курсы / Wishlist | Исключено из MVP | v1.0 |
| История просмотров курсов | Исключено из MVP | v1.5 (Analytics) |
| Уведомления в кабинете (notification center) | Исключено из MVP | Spec 10 только Telegram |
| Удаление аккаунта покупателем | Исключено из MVP | Только через поддержку |
| Смена email | Исключено из MVP | Требует верификации — v1.0 |
| Привязка социальных сетей (Google, Facebook) | Исключено из MVP | OAuth — v1.0 |
| Мобильное приложение — адаптация /me | TBD | Mobile-first дизайн требует отдельного UX-ревью |
| Экспорт данных покупателя (GDPR) | Исключено из MVP | v2.0 |
| История изменений профиля (audit log) | Исключено из MVP | EventLog — v1.5 |
| Пагинация отзывов: infinite scroll vs кнопка | TBD | Пока кнопка "Загрузить ещё" |
| Двухфакторная аутентификация | Исключено из MVP | v1.0 |
| Лимит на аватар: минимальные размеры (пиксели) | TBD | min 100×100px? Уточнить с дизайном |

---

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

| Модуль | Связь |
|--------|-------|
| **Spec 07** (Auth) | Account, авторизация, JWT, смена пароля |
| **Spec 05** (Lead Management) | Lead сущность, lead_status, seller_note |
| **Spec 01** (Seller Profile) | Данные продавца для отображения в карточках лидов |
| **Spec 02** (Item Management) | Item данные (title, slug, cover_url) |
| **Spec 14** (Reviews) | Review сущность, статусы, seller_reply |
| **Spec 12** (Public Seller Profile) | Ссылки на /sellers/[id] в карточках |
