Qadam Roadmap
проектdocs/audits/qadam-contradictions-registry.md

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

Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев

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

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

  • Статус документа: working reference
  • Актуально на: 28 марта 2026 года
  • Владелец: backend/platform-команда
  • Пересмотр: при следующем цикле аудита, security review или ревизии противоречий в документации и спеках
  • Область применения: audit-слой проекта: снимки состояния, реестры расхождений и результаты ревизий
  • Связанные документы:

Проверено: все 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 определяет:

enum ReviewStatus {
  active
  pending_moderation
  rejected
}

Spec 14 §6.1 определяет:

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 определяет:

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:

enum PriceType {
  per_lesson
  per_month
  per_package
  one_time
}

Spec 06 §6.2:

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:

enum DiscountType {
  percent
  fixed_amount
}

Spec 06 §6.2:

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:

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:

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:

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):

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):

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:

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:

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