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
}
Конфликт:
- Spec 04 имеет 3 значения, Spec 14 имеет 4 (добавлен
pending) - Spec 04 использует
active, Spec 14 используетpublished— это разные имена для одного статуса - 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вместоnewprocessingвместоcontactedrejectedвместо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-а для смены пароля:
/api/me/change-password— buyer меняет пароль (нужен current_password)/api/auth/change-temp-password— staff при первом входе (без current_password)/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 |