# Qadam MVP — Полный реестр противоречий между спеками

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

- Статус документа: working reference
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при следующем цикле аудита, security review или ревизии противоречий в документации и спеках
- Область применения: audit-слой проекта: снимки состояния, реестры расхождений и результаты ревизий
- Связанные документы:
  - [Индекс документации](../README.md)
  - [Текущее состояние](../project/current-state.md)
  - [Roadmap](../project/roadmap.md)

> Проверено: все 17 спеков перекрёстно сверены
> Найдено: 28 противоречий (7 критических, 12 средних, 9 минорных)

---

## 🔴 КРИТИЧЕСКИЕ ПРОТИВОРЕЧИЯ (блокируют разработку)

---

### C-01. Buyer Profile — две конфликтующие модели данных

**Spec 08 §5** определяет:
```
Buyer → Parent (first_name, last_name, phone, email)
Buyer → Student (first_name, last_name, birth_year, school_grade)
```

**Spec 13 §5** определяет:
```
BuyerProfile (first_name, last_name, phone, date_of_birth, city, avatar_url)
```

**Конфликт:** Это две разные Prisma-модели для одного и того же (профиль покупателя). Spec 08 хранит имя в Parent или Student. Spec 13 вводит отдельную BuyerProfile. Оба определяют один endpoint — `GET/PATCH /api/me/profile` — но с разными DTO:
- Spec 08: `BuyerProfileResponse` (includes children[], interests[])
- Spec 13: `BuyerProfileDto` (includes avatar_url, city, date_of_birth)

**Дополнительно:** Spec 13 добавляет поля `avatar_url`, `city`, `date_of_birth`, которых вообще нет в Spec 08. А Spec 08 добавляет `children[]` и `interests[]`, которых нет в Spec 13.

**Решение:** Нужна единая модель. Вариант: расширить Parent/Student из Spec 08 полями avatar_url, city, date_of_birth. Не создавать BuyerProfile как отдельную таблицу.

---

### C-02. ReviewStatus enum — разное количество значений

**Spec 04 §6.1** определяет:
```prisma
enum ReviewStatus {
  active
  pending_moderation
  rejected
}
```

**Spec 14 §6.1** определяет:
```prisma
enum ReviewStatus {
  pending
  published
  rejected
  pending_moderation
}
```

**Конфликт:**
1. Spec 04 имеет 3 значения, Spec 14 имеет 4 (добавлен `pending`)
2. Spec 04 использует `active`, Spec 14 использует `published` — это разные имена для одного статуса
3. Spec 14 добавляет `pending` (до модерации) — его нет в Spec 04

**Решение:** Использовать enum из Spec 14 (4 значения), заменить `active` на `published` в Spec 04.

---

### C-03. LeadStatus enum — несовпадение между Spec 07 и Spec 13

**Spec 07 §5 и Spec 09 §5** определяют:
```
new → contacted → enrolled → attended → no_show → purchased → not_purchased
```

**Spec 13 §6.2** определяет:
```typescript
type LeadStatus = 'pending' | 'processing' | 'enrolled' | 'attended' | 'purchased' | 'rejected'
```

**Конфликт:** Spec 13 использует совершенно другие названия:
- `pending` вместо `new`
- `processing` вместо `contacted`
- `rejected` вместо `not_purchased`
- Нет `no_show`

Один и тот же enum назван по-разному в buyer-facing и seller-facing спеках.

**Решение:** Использовать enum из Spec 07/09 (source of truth). В Spec 13 маппить на human-readable labels для UI покупателя (new → "Новая", contacted → "В обработке" и т.д.), но НЕ создавать другой enum.

---

### C-04. ItemStatus — moderation_status = "approved" vs "active"

**Spec 07 §4** (бизнес-правило №7):
```
Айтем должен быть доступен: item_isvisible = true AND moderation_status = approved AND seller.account_status = active
```

**Spec 02 §6.1, Spec 04 §6.1, Spec 05, Spec 06:**
```
moderation_status = active (не approved!)
```

**Конфликт:** Spec 07 использует `approved`, все остальные спеки используют `active`. Значения `approved` нет в ItemStatus enum ни в одном спеке.

**Решение:** Заменить `approved` на `active` в Spec 07.

---

### C-05. Просмотры (ItemView) — включены или исключены из MVP?

**Spec 11 §5** (Seller Dashboard):
```
ItemView — новая сущность MVP:
  view_id, item_id, seller_id, viewer_fingerprint, buyer_id, viewed_at
POST /api/items/:id/view — эндпоинт для трекинга
Метрика "Просмотры" — одна из 4 ключевых карточек дашборда
```

**Spec 06 §8** (Item Detail Page — TBD):
```
"Аналитика просмотров страницы (view_count) — Исключено из MVP. 
Нет таблицы ItemView. Аналитика — v1.5"
```

**Конфликт:** Spec 11 создаёт ItemView как часть MVP. Spec 06 явно исключает её из MVP. Прямое противоречие.

**Решение:** Продуктовое решение: включить или исключить. Если включить — обновить Spec 06 §8. Если исключить — убрать метрику просмотров из дашборда Spec 11.

---

### C-06. PriceType enum — разные значения в Spec 02 и Spec 06

**Spec 02 §6.1:**
```prisma
enum PriceType {
  per_lesson
  per_month
  per_package
  one_time
}
```

**Spec 06 §6.2:**
```typescript
price_type: 'per_lesson' | 'per_month' | 'subscription' | 'package'
```

**Конфликт:**
- Spec 02: `per_package` → Spec 06: `package` (без префикса)
- Spec 02: `one_time` → отсутствует в Spec 06
- Spec 06: `subscription` → отсутствует в Spec 02

**Решение:** Использовать enum из Spec 02 (source of truth для Item). Обновить DTO в Spec 06.

---

### C-07. DiscountType enum — разные значения

**Spec 02 §6.1:**
```prisma
enum DiscountType {
  percent
  fixed_amount
}
```

**Spec 06 §6.2:**
```typescript
discount_type: 'percent' | 'fixed_amount' | 'gift'
```

**Конфликт:** Spec 06 добавляет `gift`, которого нет в Spec 02. При этом Spec 02 — source of truth для модели данных ItemSpecialOffer.

**Решение:** Добавить `gift` в enum в Spec 02, или убрать из Spec 06 если эта функциональность не нужна в MVP.

---

## 🟡 СРЕДНИЕ ПРОТИВОРЕЧИЯ (не блокируют, но создают путаницу)

---

### M-01. Email обязательность при регистрации buyer

**Spec 08 §3 UC-01 (Шаг 2):**
```
Email (необязательно)
```

**Spec 08 §6.2:**
```typescript
export class RegisterBuyerAccountDto {
  @IsOptional()
  @IsEmail()
  email?: string  // необязательно
}
```

**Spec 01 §4 (бизнес-правило №4):**
```
Multi-account support — один аккаунт может быть и BUYER и SELLER
```

**Проблема:** Если buyer зарегистрировался без email, а потом хочет стать seller (multi-account, Spec 08 UC-05), ему понадобится email (обязателен для seller). Workflow для добавления email не описан ни в одном спеке.

---

### M-02. Telegram-бот — webhook vs polling

**Spec 10 §6.3** определяет:
```
POST /api/internal/telegram/webhook
Auth: Telegram secret token в заголовке X-Telegram-Bot-Api-Secret-Token
```

**Spec 01 §8 TBD:**
```
"Telegram-бот: механизм передачи chat_id — TBD. 
Бот при /start должен передавать chat_id + генерировать код в БД. 
Реализация бота — отдельная задача"
```

**Противоречие:** Spec 10 уже определяет webhook-эндпоинт с конкретной реализацией, а Spec 01 помечает весь механизм как TBD. Нужно синхронизировать — Spec 10 является решением для TBD из Spec 01.

---

### M-03. Seller Staff — кто видит список сотрудников?

**Spec 03 §4 (матрица доступов):**
```
Teacher: Просмотр списка → ❌ (нет доступа)
```

**Spec 03 §6.3:**
```
GET /api/seller/staff
→ 403: { error: 'INSUFFICIENT_ROLE' }  // Teacher не видит список
```

**Spec 03 §2 (роли):**
```
Seller Teacher: Только просмотр собственного профиля и его редактирование
```

**Spec 11 §4 (матрица дашборда):**
```
Список моих курсов: Teacher → ✅ (только свои)
```

**Противоречие не критическое**, но: Teacher не видит список сотрудников (Spec 03), но видит список курсов в которых участвует (Spec 11). При этом на странице курса (Spec 06) показываются преподаватели. Логика доступа Teacher'а размыта.

---

### M-04. Лид от гостя — привязка при регистрации

**Spec 07 §8 TBD:**
```
"Ретроспективная привязка гостевых лидов к байеру при логине — 
Исключено из MVP"
```

**Spec 08 §7 (edge cases):**
```
"Гость, оставивший лид без регистрации, потом регистрируется с тем же телефоном — 
Лиды оставленные гостем НЕ привязываются автоматически. MVP ограничение"
```

**Spec 13 §7 (edge cases):**
```
"BuyerProfile ещё не создан (новый аккаунт) — 
Создаётся при первом обращении с пустыми полями"
```

**Проблема:** Все три спека согласны, что привязка не делается. Но нет упоминания о том, что buyer увидит в `/me/leads` — только лиды созданные после регистрации? Или "Ваших заявок нет" даже если по его телефону есть 10 заявок? UX не продуман для этого сценария.

---

### M-05. Формат описания item_desc — markdown или plain text?

**Spec 06 §3 UC-01 (Block 3):**
```
"{item_desc} — полный текст, поддерживает markdown-рендеринг (bold, списки, параграфы)"
```

**Spec 02 §3 UC-01 (Шаг 1):**
```
"Полное описание * (до 5000 символов) — подробное описание на странице айтема"
```

Spec 02 не упоминает markdown. Нет markdown-редактора в форме создания. Поле описано как обычный textarea.

**Spec 14 §7 (edge cases):**
```
"Текст отзыва содержит HTML-теги → Санитизация на сервере (strip HTML), 
хранится plain text"
```

**Противоречие:** item_desc должен поддерживать markdown (Spec 06), но форма создания (Spec 02) — обычный textarea без markdown-toolbox. Если пользователь вводит markdown-синтаксис, он увидит `**жирный**` как текст в форме, а на публичной странице как **жирный**. Кроме того, отзывы strip-ают HTML — а item_desc рендерит markdown. Несогласованность подхода к rich text.

---

### M-06. Спецпредложение — condition enum конфликт

**Spec 02 §6.1:**
```prisma
enum OfferCondition {
  new_clients
  prepay
  all
}
```

**Spec 06 §5 (ItemSpecialOffer):**
```
condition: string  // Условие получения
```

**Противоречие:** Spec 02 определяет condition как enum (3 значения). Spec 06 определяет как произвольную строку. При JOIN-е данные не совпадут — один ожидает enum, другой ожидает free text.

---

### M-07. Кто может отвечать на отзывы?

**Spec 14 §3 UC-03:**
```
"Актор: Продавец (Owner / Admin CRM)"
```

**Spec 14 §6.3:**
```
PATCH /api/reviews/:review_id/reply
Auth: Bearer (seller: owner | admin_crm)
```

**Spec 03 §4 (матрица доступов — MVP):**

Нет роли "может отвечать на отзывы" в матрице Spec 03. Матрица покрывает только: создать/редактировать/блокировать/удалить сотрудников.

**Проблема:** Spec 14 разрешает Owner и AdminCRM отвечать, но Spec 03 не включает "управление отзывами" в матрицу доступов. Manager и Teacher точно не могут — но это не зафиксировано явно в Spec 03.

---

### M-08. Lead.special_offer_id — откуда берётся?

**Spec 07 §5:**
```
special_offer_id: UUID FK? → SpecialOffer, если лид создан по акции (→ Spec 02)
```

**Spec 07 §6.2:**
```typescript
export class CreateLeadDto {
  item_id: string
  lead_name: string
  lead_phone: string
  lead_email?: string
  lead_comment?: string
  lead_type: LeadType
  // НЕТ special_offer_id!
}
```

**Противоречие:** Lead модель имеет поле special_offer_id, но в CreateLeadDto его нет. Покупатель не может указать по какой акции пришёл. Как заполняется это поле? Автоматически? По какому принципу? Не определено.

---

### M-09. Seller reviews — эндпоинт определён дважды с разными ответами

**Spec 12 §6.3:**
```
GET /api/sellers/:seller_id/reviews → ReviewsPageResponse
```

**Spec 14 §6.3:**
```
GET /api/sellers/:seller_id/reviews → ReviewsListResponse
```

**Противоречие:** Один и тот же endpoint возвращает разные типы:
- Spec 12: `ReviewsPageResponse { reviews, total, has_more, next_cursor }`
- Spec 14: `ReviewsListResponse { reviews, total, has_more, next_cursor }`

Поля совпадают по содержанию, но типы названы по-разному. Формально — это два разных контракта для одного URL.

---

### M-10. SellerAddress — latitude/longitude nullable vs required

**Spec 01 §5:**
```
latitude: Decimal(10,7)  — без nullable
longitude: Decimal(10,7) — без nullable
```

**Spec 01 §6.1 (Prisma):**
```prisma
latitude  Decimal  @db.Decimal(10, 7)  — NOT NULL
longitude Decimal  @db.Decimal(10, 7)  — NOT NULL
```

**Spec 02 §5 (ItemLocation):**
```
latitude: Decimal?  — nullable
longitude: Decimal? — nullable
```

**Spec 05 §5 (ItemLocation):**
```
latitude: Decimal?  — nullable
longitude: Decimal? — nullable
```

**Противоречие:** SellerAddress требует координаты (NOT NULL), а ItemLocation допускает null. Но ItemLocation может ссылаться на SellerAddress (`seller_address_id FK`). Если адрес выбран из SellerAddress — координаты гарантированно есть. Если введён вручную ("Другой адрес") — могут быть null. Но в Spec 02 CreateItemLocationDto координаты тоже опциональны, при этом для офлайн-айтемов карта не покажет маркер без координат.

---

### M-11. Account.email — nullable vs required

**Spec 01 §5:**
```
email: string?  — nullable
```

**Spec 01 §6.1 (Prisma):**
```prisma
email  String?  @unique  — nullable
```

**Spec 01 §3 UC-01 (Шаг 2):**
```
Email * (обязательное поле для seller)
```

**Противоречие:** Модель допускает null, но UI требует обязательное заполнение при регистрации seller. Для buyer email необязателен (Spec 08). Значит email IS nullable в модели, но обязателен при создании SELLER. Это не противоречие в модели, но в DTO `RegisterAccountDto` email помечен как `@IsEmail()` без `@IsOptional()` — значит обязателен для ВСЕХ, включая buyer.

**Spec 08 §6.2:**
```typescript
export class RegisterBuyerAccountDto {
  @IsOptional() @IsEmail()
  email?: string  // optional для buyer
}
```

Но это РАЗНЫЕ DTO! Один для seller (Spec 01), другой для buyer (Spec 08). Логика верная, но при объединении в единый auth endpoint (как предлагается в roadmap) — нужна условная валидация.

---

### M-12. Максимальное количество видео

**Spec 02 §3 UC-01 (Шаг 4):**
```
"Видео (необязательно): Ссылка на YouTube или Vimeo"
(единственное число, одна ссылка)
```

**Spec 02 §3 UC-08:**
```
"Максимум 3 видео-ссылки"
```

**Spec 02 §6.3:**
```
→ 400: { error: 'MAX_VIDEOS_REACHED' }
```

**Несогласованность:** UC-01 подразумевает одно видео (singular), UC-08 говорит max 3. Не критично (UC-08 детальнее), но вводит в заблуждение при чтении UC-01.

---

## 🟢 МИНОРНЫЕ ПРОТИВОРЕЧИЯ (косметические, легко фиксятся)

---

### L-01. Spec numbering — Lead Submission

Spec 07 называется "Lead Submission (Buyer)" но в зависимостях других спеков ссылки разные:
- Spec 09 §1: "Форма оставления заявки покупателем → Spec 06 (Buyer Flow)"
- Spec 09 §9: "Spec 06 (Buyer Flow) — Создание лида покупателем"
- Spec 10 §9: "Spec 06 (Buyer Flow)"
- Spec 16 §1: "Создание лидов → Spec 09"

**Путаница:** Spec 09 ссылается на Spec 06 как "Buyer Flow" (а Spec 06 — это Item Detail Page). Lead Submission — это Spec 07, не 06.

---

### L-02. Spec 13 ссылается на несуществующие спеки

**Spec 13 §1:**
```
"Подача заявки (лида) → Spec 05 (Lead Management)"
"Авторизация и регистрация покупателя → Spec 07 (Auth)"
```

**Факт:**
- Lead Management = Spec 09 (не 05)
- Auth/Buyer Onboarding = Spec 08 (не 07)
- Spec 05 = Catalog & Search
- Spec 07 = Lead Submission

---

### L-03. Spec 12 ссылается на "Spec v1.0 (Catalog)"

**Spec 12 §1:**
```
"Каталог и поиск курсов → Spec v1.0"
```

**Факт:** Каталог — это Spec 05 (уже в MVP), не v1.0.

---

### L-04. subject_registry vs Subject

Разные спеки ссылаются на справочник предметов по-разному:
- Spec 01: `subject_registry`
- Spec 02: `subject_registry`
- Spec 15: `Subject` (модель) + `SubjectGroup`

Нет сущности `subject_registry` в Prisma — это Subject из Spec 15. Терминология не согласована.

---

### L-05. Часовые метки спецпредложений

**Spec 02 §5 (ItemSpecialOffer):**
```
starts_at: DateTime?
ends_at: DateTime?
```

**Spec 02 §6.2:**
```typescript
@IsOptional() @IsDateString()
starts_at?: string  // ISO string
```

**Spec 02 §7:**
```
"Спецпредложение с ends_at в прошлом при создании → 
400: 'Дата окончания акции не может быть в прошлом.'"
```

**Проблема:** starts_at не валидируется — можно создать акцию с starts_at в прошлом. Нет проверки starts_at < ends_at.

---

### L-06. Item.cover_image_url vs ItemMedia.sort_order[0]

**Spec 02 §5:**
```
cover_image_url: string?  — URL обложки в CDN
```

**Spec 02 §4 (бизнес-правило №6):**
```
"Порядок фото: первое фото в массиве = обложка (cover_image_url). 
При удалении обложки — следующее становится обложкой."
```

**Проблема:** cover_image_url хранится как отдельное поле в Item, но должно автоматически синхронизироваться с ItemMedia[sort_order=0].url. Два источника истины — нужен триггер/сервис для синхронизации, или убрать cover_image_url и всегда читать из ItemMedia.

---

### L-07. CPL сумма — $30 vs конфиг

**Spec 09 §1:**
```
"Продавец платит $30 за каждый доставленный лид"
```

**Spec 16 §4:**
```
"Цена лида: $30 фиксированная (год 1). Берётся из конфига CPL_PRICE_USD"
```

**Spec 07 §4:**
```
"CPL НЕ списывается за повторный лид на тот же айтем в течение 30 дней"
```

**Spec 16 не упоминает** правило 30-дневного окна из Spec 07. LeadBillingEvent создаётся атомарно с Lead — но проверка "повторный лид за 30 дней" не описана в Spec 16.

---

### L-08. Seller.telegram_channel vs Telegram notification bot

**Spec 01 §5 (SchoolProfile):**
```
telegram_channel: string?  // URL или @username канала продавца
```

**Spec 01 §5 (Seller):**
```
telegram_chat_id: BigInt?  // для уведомлений через бота
```

**Путаница:** `telegram_channel` — это публичный канал/аккаунт продавца (для отображения на профиле). `telegram_chat_id` — это chat_id для бота уведомлений. Это два разных поля с разным назначением, но оба содержат "telegram" в имени. Нужно лучше документировать различие.

---

### L-09. Buyer cabinet — change password endpoint

**Spec 13 §6.3:**
```
POST /api/me/change-password
Body: ChangeBuyerPasswordDto { current_password, new_password, confirm_password }
```

**Spec 03 §6.3:**
```
POST /api/auth/change-temp-password
Body: { new_password, confirm_password }
```

**Spec 01 §3 UC-02:**
```
POST /api/auth/reset-password
Body: ResetPasswordDto { token, new_password }
```

**Три разных endpoint-а** для смены пароля:
1. `/api/me/change-password` — buyer меняет пароль (нужен current_password)
2. `/api/auth/change-temp-password` — staff при первом входе (без current_password)
3. `/api/auth/reset-password` — восстановление по ссылке/коду (без current_password)

Не противоречие, но: seller Owner тоже может захотеть сменить пароль, а для него endpoint не определён. Нужен общий `/api/auth/change-password` с проверкой current_password.

---

## Сводка для принятия решений

| Приоритет | ID | Суть | Решение |
|---|---|---|---|
| 🔴 | C-01 | Buyer Profile: 2 модели | Объединить в одну (расширить Parent/Student) |
| 🔴 | C-02 | ReviewStatus: 3 vs 4 значения, active vs published | Использовать 4 из Spec 14 (pending/published/rejected/pending_moderation) |
| 🔴 | C-03 | LeadStatus: разные имена в Spec 07 vs 13 | Один enum из Spec 07, в Spec 13 маппить на labels |
| 🔴 | C-04 | moderation_status: approved vs active | Заменить approved на active в Spec 07 |
| 🔴 | C-05 | ItemView: MVP vs исключено из MVP | Продуктовое решение (включить → обновить Spec 06) |
| 🔴 | C-06 | PriceType: per_package vs package, subscription vs one_time | Использовать enum из Spec 02 |
| 🔴 | C-07 | DiscountType: gift отсутствует в Spec 02 | Добавить gift в Spec 02 или убрать из Spec 06 |
| 🟡 | M-01 | Buyer без email → хочет стать seller | Описать workflow добавления email |
| 🟡 | M-02 | Telegram webhook: TBD в Spec 01 vs определён в Spec 10 | Spec 10 закрывает TBD из Spec 01 — синхронизировать |
| 🟡 | M-03 | Teacher доступ к списку сотрудников | Уточнить и зафиксировать в Spec 03 |
| 🟡 | M-04 | Гостевые лиды не привязываются | Описать UX для buyer с лидами до регистрации |
| 🟡 | M-05 | item_desc: markdown vs plain text | Решить: если markdown → добавить editor в Spec 02 |
| 🟡 | M-06 | OfferCondition: enum vs string | Использовать enum из Spec 02 |
| 🟡 | M-07 | Кто отвечает на отзывы | Добавить в матрицу Spec 03 |
| 🟡 | M-08 | special_offer_id не в CreateLeadDto | Решить: автоматически или убрать из Lead |
| 🟡 | M-09 | Два типа для одного reviews endpoint | Унифицировать название DTO |
| 🟡 | M-10 | SellerAddress lat/lon NOT NULL vs ItemLocation nullable | Документировать разницу и добавить валидацию |
| 🟡 | M-11 | Account.email nullable vs required | Условная валидация по account_type |
| 🟡 | M-12 | Количество видео: 1 vs 3 | Уточнить UC-01 (max 3) |
| 🟢 | L-01 | Неверные ссылки на спеки в Spec 09 | Исправить ссылки |
| 🟢 | L-02 | Неверные ссылки в Spec 13 | Исправить ссылки |
| 🟢 | L-03 | Spec 12 ссылается на v1.0 вместо Spec 05 | Исправить ссылку |
| 🟢 | L-04 | subject_registry vs Subject | Унифицировать терминологию |
| 🟢 | L-05 | starts_at не валидируется | Добавить starts_at < ends_at |
| 🟢 | L-06 | cover_image_url дублирует ItemMedia[0] | Убрать поле или добавить синхронизацию |
| 🟢 | L-07 | 30-дневное окно CPL не в Spec 16 | Добавить правило в Spec 16 |
| 🟢 | L-08 | telegram_channel vs telegram_chat_id | Документировать различие |
| 🟢 | L-09 | 3 endpoint-а для смены пароля | Унифицировать в один /api/auth/change-password |
