API Requirements — Регистрация (Buyer + Seller)
Обновлён 14 апр. 2026 г., 17:16 · 0 комментариев
API Requirements — Регистрация (Buyer + Seller)
Паспорт документа
- Статус документа: target requirements
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении целевых продуктовых требований, backend-контракта или registration flow
- Область применения: целевые требования к крупным прикладным backend/API потокам
- Связанные документы:
Источники: Spec 01 (Seller Onboarding), Spec 08 (Buyer Onboarding) Дата: 2026-03-28 Статус: backend-контур реализован и используется как source of truth в
qadam-coreЦель: каноническая backend-дока по registration/profile flow и остаточному техдолгу
Важно
- Реальный API публикуется под глобальным prefix
/api/v1. Ниже в текстовых примерах местами сохраняется краткая продуктовая форма/api/..., но для frontend-команды источником истины считается OpenAPI artifactapps/api/openapi/openapi.jsonи production URLhttps://qadam.2fab.app/api/openapi.json. - Реальные controller responses во многих местах используют envelope-форму:
{ user },{ buyer },{ seller },{ profile },{ children },{ child },{ addresses },{ address }. Для интеграции и codegen нужно ориентироваться на OpenAPI, а не на сокращённые примеры из этой доки. PUT /me/profileиPUT /seller/profileсохранены как legacy aliases. Канонический write-contract для новых клиентов —PATCH.
1. Текущее состояние
1.1 Что уже есть
Статус исполнения
- Реализованы
POST /api/v1/auth/check-availability,POST /api/v1/auth/register/buyer,POST /api/v1/auth/register/seller. - Реализованы password reset endpoints:
POST /api/v1/auth/forgot-password,POST /api/v1/auth/verify-reset-code,POST /api/v1/auth/reset-password. - Реализован авторизованный password settings endpoint:
POST /api/v1/auth/change-password. - Реализован multi-role сценарий
POST /api/v1/auth/add-buyer-role. - Реализованы buyer profile endpoints:
GET/POST/PATCH /api/v1/me/profile, children CRUD иPATCH /api/v1/me/interests. - Реализованы seller profile endpoints:
GET/POST/PATCH /api/v1/seller/profile, addresses CRUD,PATCH /api/v1/seller/addresses/:addressId/set-primary,POST /api/v1/seller/telegram/verify,DELETE /api/v1/seller/telegram. - Реализован публичный справочник
GET /api/v1/subjects. - Реализован admin endpoint
PATCH /api/v1/admin/sellers/:sellerId/status. - Реализован
POST /api/v1/upload/imageс локальным storage и публичной раздачей/uploads/images/....
Legacy API, оставленное для совместимости
| Метод | Путь | Статус |
|---|---|---|
| POST | /api/v1/auth/register | legacy route, сохранён для обратной совместимости |
| PUT | /api/v1/seller/profile | legacy alias для PATCH /api/v1/seller/profile |
| PUT | /api/v1/me/profile | legacy alias для PATCH /api/v1/me/profile |
Текущее DTO legacy-регистрации
RegisterSchema {
firstName?: string
lastName?: string
email?: string
phone: string // required, +998XXXXXXXXX
password: string // 8-128 символов, минимум 1 цифра
type: 'BUYER' | 'SELLER'
}
Текущая модель Account (Prisma)
Account {
id String @id
firstName String
lastName String
type AccountType // BUYER | SELLER | SELLER_STAFF | ADMIN
email String? @unique
phone String @unique
passwordHash String
status AccountStatus // ACTIVE | UNDER_REVIEW | BLOCKED
lastLoginAt DateTime?
createdAt DateTime
updatedAt DateTime
}
Текущая модель Buyer и связанных сущностей
Buyer {
id String @id
accountId String @unique
type BuyerType // PARENT | STUDENT
}
Текущая модель Parent:
Parent {
id String @id
buyerId String @unique
name String // legacy compatibility field
firstName String
lastName String
phone String
email String?
}
Текущая модель Student:
Student {
id String @id
buyerId String? @unique
parentId String? // legacy compatibility field
name String // legacy compatibility field
gender String? // legacy compatibility field
birthdate DateTime? // legacy compatibility field
firstName String
lastName String?
birthYear Int?
class Int?
}
Текущая модель Seller и связанных сущностей
Seller {
id String @id
accountId String @unique
type SellerType // SCHOOL_OFFLINE | ONLINE_SCHOOL | INDIVIDUAL_CONTRIBUTOR
telegramChatId BigInt?
telegramVerifiedAt DateTime?
}
Текущие профильные модели продавца
- Функционально backend уже поддерживает
shortDesc,fullDesc, social links, logo/photo, addresses и subject links. - На уровне Prisma schema legacy naming всё ещё сохранён для совместимости:
nameиdescriptionпока не вытеснены окончательноorgName/fullDesc/bio. phoneиemailв профильных таблицах по-прежнему nullable на уровне БД. На уровне API contract они обязательны там, где это требуется продуктовым сценарием.
1.2 Чего ещё нет или не доведено
- SMS/email transport пока stub-овый: reset token и SMS code создаются корректно, но внешняя доставка ещё не подключена к реальному провайдеру.
- Upload storage пока локальный (
/var/lib/qadam-core/uploads), а не S3/CDN. - Data model cleanup не завершён: legacy compatibility fields (
Parent.name,Student.parentId/name/gender/birthdate,School.description,IndividualContributor.name/description) всё ещё присутствуют. - Веб-интеграция нового registration/profile flow ведётся отдельной frontend-командой в
qadam-webи синхронизируется через OpenAPI contract.
2. Изменения в модели данных
2.1 Account — новые поля
model Account {
id String @id
+ firstName String @db.VarChar(50) // 2-50 символов
+ lastName String @db.VarChar(50) // 2-50 символов
type AccountType
email String? @unique // ИЗМЕНЕНИЕ: сделать nullable (для buyer email необязателен)
phone String @unique
passwordHash String
status AccountStatus
lastLoginAt DateTime?
createdAt DateTime
updatedAt DateTime
}
Важно: По спеку buyer может регистрироваться без email (только телефон). Значит email должен стать nullable на уровне Account.
2.2 AccountStatus — изменение enum
enum AccountStatus {
ACTIVE
- SUSPENDED
- DELETED
+ UNDER_REVIEW
+ BLOCKED
}
Закрыто: enum уже мигрирован в Prisma и production БД. Исторические значения SUSPENDED и DELETED были замапплены в BLOCKED во время миграции 20260328140000_registration_flow_upgrade.
2.3 Parent — разделить name на firstName + lastName
model Parent {
id String @id
buyerId String @unique
- name String
+ firstName String @db.VarChar(50)
+ lastName String @db.VarChar(50)
phone String
email String?
updatedAt DateTime @updatedAt
+ children ParentStudentLink[]
}
2.4 Student — изменения полей
model Student {
id String @id
buyerId String? @unique
- parentId String?
- name String
+ firstName String @db.VarChar(50)
+ lastName String? @db.VarChar(50) // необязательная для детей
- gender String?
- birthdate DateTime?
+ birthYear Int? // вместо полной даты — только год
class Int? // 1-11 (спек называет school_grade)
updatedAt DateTime @updatedAt
+ parents ParentStudentLink[]
+ subjects StudentSubjectLink[]
}
2.5 Новая модель: ParentStudentLink
model ParentStudentLink {
id String @id @default(uuid())
parentId String
studentId String
parent Parent @relation(fields: [parentId], references: [id])
student Student @relation(fields: [studentId], references: [id])
@@unique([parentId, studentId])
}
Сейчас нет. Текущая связь Parent ↔ Student через Student.parentId — заменить на связь через таблицу.
2.6 Новая модель: StudentSubjectLink
model StudentSubjectLink {
id String @id @default(uuid())
studentId String
subjectId String
student Student @relation(fields: [studentId], references: [id])
@@unique([studentId, subjectId])
}
2.7 SchoolProfile — расширение полей
model School {
id String @id
sellerId String @unique
- name String
- phone String?
- email String?
- description String?
- locationId String?
+ orgName String @db.VarChar(200) // required, 3-200
+ shortDesc String @db.VarChar(150) // required, 10-150 (НОВОЕ)
+ fullDesc String @db.Text // required, 20-2000 (вместо description)
+ phone String // required (не optional)
+ email String // required (не optional)
+ websiteUrl String? // НОВОЕ
+ instagramUrl String? // НОВОЕ
+ telegramChannel String? // НОВОЕ
+ logoUrl String? // НОВОЕ
updatedAt DateTime @updatedAt
}
2.8 OnlineSchool — аналогичное расширение
Те же новые поля что и School (orgName, shortDesc, fullDesc, social links, logoUrl).
2.9 IndividualContributor — расширение полей
model IndividualContributor {
id String @id
sellerId String @unique
- name String
- phone String?
- email String?
- description String?
- locationId String?
+ firstName String // вместо name
+ lastName String
+ tagline String @db.VarChar(150) // НОВОЕ, 10-150
+ bio String @db.Text // НОВОЕ, вместо description, 20-2000
+ phone String // required
+ email String // required
+ workFormat WorkFormat // НОВОЕ: online / offline / mobile_tutor
+ photoUrl String? // НОВОЕ
+ instagramUrl String? // НОВОЕ
+ telegramChannel String? // НОВОЕ
updatedAt DateTime @updatedAt
}
2.10 Новый enum: WorkFormat
enum WorkFormat {
ONLINE
OFFLINE
MOBILE_TUTOR
}
2.11 Seller — новые поля
model Seller {
id String @id
accountId String @unique
type SellerType
+ telegramChatId BigInt? // НОВОЕ
+ telegramVerifiedAt DateTime? // НОВОЕ
createdAt DateTime
// relations
+ addresses SellerAddress[]
+ subjects SellerSubjectLink[]
+ telegramCodes TelegramVerificationCode[]
}
2.12 Новая модель: SellerAddress
model SellerAddress {
id String @id @default(uuid())
sellerId String
branchName String?
city String
fullAddress String
latitude Decimal @db.Decimal(10, 7)
longitude Decimal @db.Decimal(10, 7)
displayPublicly Boolean @default(true)
isPrimary Boolean @default(false)
orderIndex Int @default(0)
createdAt DateTime @default(now())
seller Seller @relation(fields: [sellerId], references: [id])
}
2.13 Новая модель: SellerSubjectLink
model SellerSubjectLink {
id String @id @default(uuid())
sellerId String
subjectId String
seller Seller @relation(fields: [sellerId], references: [id])
@@unique([sellerId, subjectId])
}
2.14 Новая модель: PasswordResetToken
model PasswordResetToken {
token String @id @default(uuid())
accountId String
type ResetType // EMAIL_LINK | SMS_CODE
code String? // 6-значный, только для SMS
expiresAt DateTime
used Boolean @default(false)
attempts Int @default(0)
createdAt DateTime @default(now())
account Account @relation(fields: [accountId], references: [id])
}
enum ResetType {
EMAIL_LINK
SMS_CODE
}
2.15 Новая модель: TelegramVerificationCode
model TelegramVerificationCode {
code String @id // 6-значный числовой
sellerId String
chatId BigInt
expiresAt DateTime
used Boolean @default(false)
seller Seller @relation(fields: [sellerId], references: [id])
}
3. Новые и изменённые эндпоинты
3.0 Порядок вызовов при регистрации (Flow)
Регистрация — 4-шаговый wizard на фронте. API вызывается на двух шагах:
ШАГ 1 — Выбор роли (buyer / seller)
→ Нет API-вызовов. Только фронт.
ШАГ 2 — Данные аккаунта (firstName, lastName, phone, email, password)
→ При нажатии "Продолжить":
┌─────────────────────────────────────────────────────────────┐
│ POST /api/auth/check-availability { phone, email? } │
│ │
│ Проверяет уникальность email и/или phone ДО регистрации. │
│ Если 409 EMAIL_TAKEN или PHONE_TAKEN — фронт показывает │
│ ошибку на конкретном поле + ссылки "Войти" / │
│ "Восстановить пароль". │
│ Если 200 — фронт переходит к Шагу 3. │
│ │
│ Для seller: email обязателен → проверяем оба. │
│ Для buyer: email опционален → проверяем phone, │
│ email только если указан. │
└─────────────────────────────────────────────────────────────┘
ШАГ 3 — Выбор подтипа (sellerType / buyerType)
→ Нет API-вызовов. Только фронт.
ШАГ 4 — Профиль / Онбординг
→ При нажатии "Завершить регистрацию":
┌─────────────────────────────────────────────────────────────┐
│ POST /api/auth/register/seller (для продавца) │
│ POST /api/auth/register/buyer (для байера) │
│ │
│ Один запрос создаёт ВСЁ: Account + Seller/Buyer + │
│ Profile + Addresses + SubjectLinks + Tokens. │
│ Данные со всех 4 шагов отправляются разом. │
│ │
│ Всё равно проверяет уникальность повторно (race condition). │
│ Возвращает 409 EMAIL_TAKEN / PHONE_TAKEN если дубликат. │
└─────────────────────────────────────────────────────────────┘
Важно: check-availability — это pre-validation. Финальная проверка уникальности всё равно происходит в register/seller и register/buyer (уникальный индекс в БД ловит race conditions). Но без check-availability пользователь узнает о дубликате только на последнем шаге, потеряв время на заполнение профиля.
3.1 Регистрация — новые эндпоинты
POST /api/auth/check-availability — Проверка уникальности (Шаг 2)
Вызывается фронтом на Шаге 2 при нажатии "Продолжить", ДО финальной регистрации.
// Request
interface CheckAvailabilityRequest {
email?: string // проверить если передан
phone?: string // проверить если передан
}
// Response 200 — всё свободно
interface CheckAvailabilityResponse {
emailAvailable?: boolean
phoneAvailable?: boolean
}
// Ошибки
// 409: { error: 'EMAIL_TAKEN', message: 'Пользователь с таким email уже зарегистрирован.' }
// 409: { error: 'PHONE_TAKEN', message: 'Аккаунт с таким номером уже существует.' }
Логика:
- Если передан
phone— проверить Account.phone на уникальность - Если передан
email— проверить Account.email на уникальность - Если оба переданы — проверить оба; при двух дубликатах вернуть первый найденный (phone приоритетнее)
- Эндпоинт публичный (без авторизации)
- Rate limiting: рекомендуется ограничить по IP (защита от перебора)
POST /api/auth/register/seller — Регистрация продавца (единый)
Заменяет текущий двухэтапный процесс (register + create profile). Один запрос создаёт всё.
// Request
interface RegisterSellerRequest {
// Аккаунт (Шаг 2)
firstName: string // 2-50
lastName: string // 2-50
phone: string // +998XXXXXXXXX, unique
email: string // valid email, unique
password: string // min 8 символов, min 1 цифра
// Тип продавца (Шаг 3)
sellerType: 'SCHOOL_OFFLINE' | 'ONLINE_SCHOOL' | 'INDIVIDUAL_CONTRIBUTOR'
// Профиль (Шаг 4) — зависит от sellerType
orgName: string // 3-200 (для school/online_school)
// ИЛИ firstName + lastName уже есть (для individual_contributor)
shortDesc: string // 10-150
fullDesc: string // 20-2000
contactPhone: string // предзаполнен из phone, может отличаться
contactEmail: string // предзаполнен из email, может отличаться
subjectIds: string[] // UUID[], min 1, max 10
// Опциональные
websiteUrl?: string
instagramUrl?: string
telegramChannel?: string
logoUrl?: string
// Только для individual_contributor
workFormat?: 'ONLINE' | 'OFFLINE' | 'MOBILE_TUTOR'
// Адреса (required для SCHOOL_OFFLINE; optional для IC с OFFLINE)
addresses?: {
branchName?: string
city: string
fullAddress: string
latitude: number // 37.0 - 45.6 (bounds Узбекистана)
longitude: number // 55.9 - 73.2
displayPublicly?: boolean // default: true
}[]
}
// Response 201
interface RegisterSellerResponse {
accessToken: string // также в httpOnly cookie qadam_at
refreshToken: string // также в httpOnly cookie qadam_rt (только для mobile)
seller: SellerProfileResponse
}
// Ошибки
// 409: { error: 'EMAIL_TAKEN', message: 'Пользователь с таким email уже зарегистрирован.' }
// 409: { error: 'PHONE_TAKEN', message: 'Аккаунт с таким номером уже существует.' }
// 422: { errors: [{ field: string, message: string }] }
Что создаётся на сервере (транзакция):
- Account (firstName, lastName, phone, email, passwordHash, type=SELLER, status=ACTIVE)
- Seller (sellerType)
- SchoolProfile / OnlineSchoolProfile / IndividualContributorProfile (в зависимости от sellerType)
- SellerAddress[] (если переданы)
- SellerSubjectLink[] (по subjectIds)
- JWT tokens (access + refresh)
POST /api/auth/register/buyer — Регистрация байера (единый)
// Request
interface RegisterBuyerRequest {
// Аккаунт (Шаг 2)
firstName: string // 2-50
lastName: string // 2-50
phone: string // +998XXXXXXXXX, unique
email?: string // optional для байера, valid email, unique если указан
password: string // min 8 символов, min 1 цифра
// Тип байера (Шаг 3)
buyerType: 'PARENT' | 'STUDENT'
// Онбординг (Шаг 4) — зависит от buyerType, ВСЕ поля опциональны
// Для parent:
children?: {
firstName: string // 2-50
age?: number // 3-18
schoolGrade?: number // 1-11
}[] // max 5
// Для student:
birthYear?: number // 1950 - (текущий год - 5)
schoolGrade?: number // 1-11
subjectIds?: string[] // UUID[], max 5
}
// Response 201
interface RegisterBuyerResponse {
accessToken: string
refreshToken: string // только для mobile
buyer: BuyerProfileResponse
}
// Ошибки
// 409: { error: 'PHONE_TAKEN', message: 'Аккаунт с таким номером уже существует.' }
// 409: { error: 'EMAIL_TAKEN', message: 'Пользователь с таким email уже зарегистрирован.' }
// 422: { errors: [{ field: string, message: string }] }
Что создаётся на сервере (транзакция):
- Account (firstName, lastName, phone, email?, passwordHash, type=BUYER, status=ACTIVE)
- Buyer (buyerType)
- Parent (firstName, lastName, phone, email) — если buyerType=PARENT
- Student[] + ParentStudentLink[] — если переданы children
- Student (firstName, lastName, birthYear, schoolGrade) — если buyerType=STUDENT
- StudentSubjectLink[] — если переданы subjectIds
- JWT tokens
3.2 Восстановление пароля — 3 новых эндпоинта
Используются и для seller, и для buyer. Единый flow.
POST /api/auth/forgot-password
// Request
interface ForgotPasswordRequest {
identifier: string // email или телефон — определяем по формату
}
// Response 200
interface ForgotPasswordResponse {
method: 'sms' | 'email'
maskedIdentifier: string // '+998 99 ***-**-12' или 'us***@mail.ru'
token: string // session token для следующих шагов
}
// Ошибки
// 404: { error: 'ACCOUNT_NOT_FOUND', message: 'Аккаунт с такими данными не найден.' }
// 429: { error: 'RATE_LIMIT', message: 'Слишком много запросов.', retryAfter: number }
Логика:
- Если identifier содержит
@→ email flow → отправить email со ссылкой (TTL 30 мин) - Если +998... → SMS flow → отправить 6-значный код (TTL 10 мин)
- Rate limit: max 3 SMS на один номер за 10 минут
- Создаёт PasswordResetToken в БД
- В текущем MVP transport остаётся stub-овым: backend создаёт токен/код, а внешняя отправка пока подменена логированием до подключения реального провайдера
POST /api/auth/verify-reset-code
Только для SMS-flow (шаг 2 — ввод кода).
// Request
interface VerifyResetCodeRequest {
code: string // 6 цифр
token: string // session token из шага 1
}
// Response 200
{ valid: true }
// Ошибки
// 400: { error: 'CODE_INVALID', message: 'Неверный код.' , attemptsLeft: number }
// 400: { error: 'CODE_EXPIRED', message: 'Код устарел. Запросите новый.' }
// 400: { error: 'TOO_MANY_ATTEMPTS', message: 'Слишком много неверных попыток. Запросите новый код через 5 мин.' }
Логика:
- Проверить PasswordResetToken по token
- Сверить code
- Max 3 попытки, потом блокировка на 5 мин
- Инкремент attempts при неверном коде
POST /api/auth/reset-password
Финальный шаг — установка нового пароля.
// Request
interface ResetPasswordRequest {
token: string // из шага 1 (email-flow) или шага 2 (sms-flow)
newPassword: string // min 8 символов, min 1 цифра
}
// Response 200
interface ResetPasswordResponse {
accessToken: string // автоматический вход после сброса
}
// Ошибки
// 400: { error: 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'TOKEN_USED', message: string }
// 422: { errors: [{ field: 'newPassword', message: string }] }
Логика:
- Проверить PasswordResetToken (не expired, не used)
- Для email-flow: token приходит из URL (/reset-password?token=...)
- Для sms-flow: token тот же что из шага 1
- Обновить passwordHash в Account
- Пометить токен как used=true
- Выдать JWT tokens (автологин)
3.3 Multi-account — добавление buyer-роли
POST /api/auth/add-buyer-role
Для авторизованного seller/seller_staff — добавить профиль покупателя.
// Auth: Bearer (SELLER или SELLER_STAFF)
// Request
interface AddBuyerRoleRequest {
buyerType: 'PARENT' | 'STUDENT'
// Для parent (optional):
children?: {
firstName: string
age?: number
schoolGrade?: number
}[]
// Для student (optional):
birthYear?: number
schoolGrade?: number
subjectIds?: string[]
}
// Response 201
BuyerProfileResponse
// Ошибки
// 409: { error: 'BUYER_ALREADY_EXISTS', message: 'У вашего аккаунта уже есть профиль покупателя.' }
Логика:
- Проверить что Buyer для этого accountId НЕ существует
- Создать Buyer + Parent/Student (аналогично регистрации, но без создания Account)
- Account.type НЕ меняется — остаётся SELLER
- Проверка ролей: по наличию Buyer/Seller записей, не только по account_type
3.4 Профиль байера — изменения существующих эндпоинтов
GET /api/me/profile — расширить response
interface BuyerProfileResponse {
buyerId: string
buyerType: 'PARENT' | 'STUDENT'
firstName: string
lastName: string
phone: string
email: string | null
accountStatus: 'ACTIVE' | 'UNDER_REVIEW' | 'BLOCKED'
// Для parent:
children?: ChildProfileDto[]
// Для student:
birthYear?: number | null
schoolGrade?: number | null
interests?: SubjectShortDto[]
}
interface ChildProfileDto {
studentId: string
firstName: string
lastName: string | null
age: number | null
schoolGrade: number | null
interests: SubjectShortDto[]
}
PATCH /api/me/profile (заменить PUT → PATCH)
interface UpdateBuyerProfileRequest {
firstName?: string // 2-50
lastName?: string // 2-50
phone?: string // +998XXXXXXXXX
email?: string // valid email
}
// Ошибки
// 400: { error: 'BUYER_TYPE_IMMUTABLE', message: 'Тип аккаунта нельзя изменить.' }
// 409: { error: 'PHONE_TAKEN' | 'EMAIL_TAKEN' }
3.5 Дети (Parent) — новые эндпоинты
GET /api/me/children
Auth: Bearer (buyer, buyerType=PARENT)
Response 200: ChildProfileDto[]
403 если buyerType ≠ PARENT
POST /api/me/children
interface CreateChildRequest {
firstName: string // 2-50
lastName?: string // max 50
age?: number // 3-18
schoolGrade?: number // 1-11
subjectIds?: string[] // max 5
}
// Response 201: ChildProfileDto
// 400: { error: 'MAX_CHILDREN_REACHED', message: 'Максимум 5 детей.' }
// 403 если buyerType ≠ PARENT
PATCH /api/me/children/:studentId
// Body: Partial<CreateChildRequest>
// Response 200: ChildProfileDto
// 404: { error: 'CHILD_NOT_FOUND' }
// 403 если чужой ребёнок
DELETE /api/me/children/:studentId
Response 204
404: { error: 'CHILD_NOT_FOUND' }
403 если чужой ребёнок
3.6 Интересы студента — новый эндпоинт
PATCH /api/me/interests
// Auth: Bearer (buyer, buyerType=STUDENT)
interface UpdateInterestsRequest {
subjectIds: string[] // полная замена, max 5
}
// Response 200: { interests: SubjectShortDto[] }
// 400: { error: 'MAX_INTERESTS_EXCEEDED', message: 'Выберите не более 5 направлений.' }
// 403 если buyerType ≠ STUDENT
3.7 Профиль продавца — изменения существующих эндпоинтов
POST /seller/profile — расширить DTO
Если оставляем раздельный flow (не объединяем с register/seller):
interface CreateSellerProfileRequest {
sellerType: 'SCHOOL_OFFLINE' | 'ONLINE_SCHOOL' | 'INDIVIDUAL_CONTRIBUTOR'
// Для school/online_school:
orgName: string // 3-200
// Для individual_contributor:
// firstName + lastName берутся из Account
shortDesc: string // 10-150
fullDesc: string // 20-2000
phone: string // required
email: string // required
subjectIds: string[] // min 1, max 10
// Optional
websiteUrl?: string
instagramUrl?: string
telegramChannel?: string
logoUrl?: string
// Только для IC
workFormat?: 'ONLINE' | 'OFFLINE' | 'MOBILE_TUTOR'
// Адреса
addresses?: CreateAddressDto[]
}
PATCH /seller/profile (заменить PUT → PATCH)
interface UpdateSellerProfileRequest {
orgName?: string // 3-200
shortDesc?: string // 10-150
fullDesc?: string // 20-2000
phone?: string
email?: string
websiteUrl?: string
instagramUrl?: string
telegramChannel?: string
logoUrl?: string
subjectIds?: string[] // min 1, max 10 (полная замена)
workFormat?: WorkFormat // только для IC
}
// Ошибки
// 400: { error: 'SELLER_TYPE_IMMUTABLE' }
// 409: { error: 'PHONE_TAKEN' | 'EMAIL_TAKEN' }
GET /seller/profile — расширить response
interface SellerProfileResponse {
sellerId: string
sellerType: SellerType
accountStatus: AccountStatus
// Из профильной таблицы:
orgName: string // или firstName + lastName для IC
shortDesc: string
fullDesc: string
phone: string
email: string
websiteUrl: string | null
instagramUrl: string | null
telegramChannel: string | null
logoUrl: string | null
// Только для IC:
workFormat?: WorkFormat
subjects: SubjectShortDto[]
addresses: SellerAddressDto[]
telegramConnected: boolean
}
interface SellerAddressDto {
addressId: string
branchName: string | null
city: string
fullAddress?: string // только если displayPublicly = true
latitude?: number // только если displayPublicly = true
longitude?: number
isPrimary: boolean
}
3.8 Адреса продавца — 5 новых эндпоинтов
GET /api/seller/addresses → SellerAddressDto[]
POST /api/seller/addresses → SellerAddressDto (201)
PATCH /api/seller/addresses/:addressId → SellerAddressDto
DELETE /api/seller/addresses/:addressId → 204
PATCH /api/seller/addresses/:addressId/set-primary → SellerAddressDto
Бизнес-правила:
- Max 10 адресов на продавца → 400
MAX_ADDRESSES_REACHED - Нельзя удалить единственный адрес (для school_offline) → 400
CANNOT_DELETE_ONLY_ADDRESS - Первый адрес автоматически isPrimary
- При удалении primary — следующий по orderIndex становится primary
- Координаты в bounds Узбекистана (lat 37-45.6, lon 55.9-73.2) → 400
INVALID_COORDINATES - Доступно только для SCHOOL_OFFLINE и IC с workFormat=OFFLINE → 403
SELLER_TYPE_NOT_ALLOWED
3.9 Telegram верификация — 2 новых эндпоинта
POST /api/seller/telegram/verify Auth: Bearer (seller)
DELETE /api/seller/telegram Auth: Bearer (seller)
POST /api/seller/telegram/verify
interface TelegramVerifyRequest {
code: string // 6 цифр
}
// Response 200: { success: true, username: string | null }
// Ошибки:
// 400: { error: 'CODE_INVALID' }
// 400: { error: 'CODE_EXPIRED' }
// 400: { error: 'TELEGRAM_ALREADY_BOUND', message: 'Этот Telegram уже используется другой организацией.' }
Логика:
- Найти TelegramVerificationCode по code
- Проверить expiresAt > now(), used = false
- Проверить chatId не привязан к другому sellerId
- Обновить Seller.telegramChatId и telegramVerifiedAt
- Пометить код used=true
- Если upstream Telegram flow сохранил username, вернуть его в response; иначе вернуть
username: null
DELETE /api/seller/telegram
Отключает Telegram: Seller.telegramChatId = null, telegramVerifiedAt = null.
3.10 Admin: управление статусом — 1 новый эндпоинт
PATCH /api/admin/sellers/:sellerId/status
Auth: Bearer (admin)
interface UpdateSellerStatusRequest {
status: 'ACTIVE' | 'UNDER_REVIEW' | 'BLOCKED'
reason?: string
}
// Response 200: { sellerId: string, accountStatus: AccountStatus }
// 400: { error: 'INVALID_STATUS_TRANSITION' }
// 404: { error: 'SELLER_NOT_FOUND' }
Валидные переходы:
- ACTIVE → UNDER_REVIEW
- ACTIVE → BLOCKED
- UNDER_REVIEW → ACTIVE
- UNDER_REVIEW → BLOCKED
- BLOCKED → ACTIVE
3.11 Справочник направлений — 1 новый эндпоинт
GET /api/subjects
Публичный (без авторизации). Возвращает список направлений для мультиселекта.
interface SubjectShortDto {
id: string
name: string
// может быть также category/group для группировки
}
// Response 200: SubjectShortDto[]
На текущем backend это уже закрыто через модель
Subjectв Prisma и seed/reference data слой.
3.12 Загрузка изображений — 1 новый эндпоинт
POST /api/upload/image
Auth: Bearer
Content-Type: multipart/form-data
// Body: file (multipart)
// Валидация: JPG/PNG/WebP, max 5MB, min 200x200px
// Response 200: { url: string }
// Ошибки:
// 400: { error: 'FILE_TOO_LARGE', message: 'Максимальный размер — 5 МБ.' }
// 400: { error: 'INVALID_FORMAT', message: 'Допустимые форматы: JPG, PNG, WebP.' }
// 400: { error: 'IMAGE_TOO_SMALL', message: 'Минимальный размер: 200×200 пикселей.' }
Текущий production-safe MVP использует локальный storage в
/var/lib/qadam-core/uploads. Следующий этап инфраструктуры: выделить storage adapter и S3/CDN driver без изменения API-контракта.
4. Изменения валидации
4.1 Пароль
Реализовано: min 8 символов + min 1 цифра
Изменить в packages/shared/src/schemas/auth.ts.
4.2 Коды ошибок при дубликатах
Реализовано: отдельные EMAIL_TAKEN и PHONE_TAKEN
Фронту нужно знать какое именно поле дублируется, чтобы подсветить его.
4.3 Токены — мобильный vs веб
Текущая логика (сохранить):
- Web: httpOnly cookies (qadam_at, qadam_rt)
- Mobile (X-Client-Type: mobile): tokens в body
5. Сводная таблица новых эндпоинтов
| # | Метод | Путь | Auth | Описание |
|---|---|---|---|---|
| 1 | POST | /api/auth/register/seller | Public | Регистрация продавца (единый) |
| 2 | POST | /api/auth/register/buyer | Public | Регистрация байера (единый) |
| 3 | POST | /api/auth/check-availability | Public | Проверка email/phone |
| 4 | POST | /api/auth/forgot-password | Public | Начать восстановление пароля |
| 5 | POST | /api/auth/verify-reset-code | Public | Проверить SMS-код |
| 6 | POST | /api/auth/reset-password | Public | Установить новый пароль |
| 7 | POST | /api/auth/add-buyer-role | Seller | Добавить buyer-роль |
| 8 | GET | /api/me/children | Buyer(parent) | Список детей |
| 9 | POST | /api/me/children | Buyer(parent) | Добавить ребёнка |
| 10 | PATCH | /api/me/children/:id | Buyer(parent) | Обновить ребёнка |
| 11 | DELETE | /api/me/children/:id | Buyer(parent) | Удалить ребёнка |
| 12 | PATCH | /api/me/interests | Buyer(student) | Обновить интересы |
| 13 | GET | /api/seller/addresses | Seller | Список адресов |
| 14 | POST | /api/seller/addresses | Seller | Добавить адрес |
| 15 | PATCH | /api/seller/addresses/:id | Seller | Обновить адрес |
| 16 | DELETE | /api/seller/addresses/:id | Seller | Удалить адрес |
| 17 | PATCH | /api/seller/addresses/:id/set-primary | Seller | Сделать основным |
| 18 | POST | /api/seller/telegram/verify | Seller | Верифицировать Telegram |
| 19 | DELETE | /api/seller/telegram | Seller | Отключить Telegram |
| 20 | PATCH | /api/admin/sellers/:id/status | Admin | Изменить статус |
| 21 | GET | /api/subjects | Public | Справочник направлений |
| 22 | POST | /api/upload/image | Auth | Загрузка изображения |
6. Изменения существующих эндпоинтов
| Эндпоинт | Что изменить |
|---|---|
POST /auth/register | Оставить для обратной совместимости или удалить. Новые: /auth/register/seller и /auth/register/buyer |
POST /seller/profile | Расширить DTO (shortDesc, fullDesc, subjects, socials, addresses, workFormat) |
PUT /seller/profile → PATCH | Сменить метод, расширить DTO |
GET /seller/profile | Расширить response (subjects, addresses, socials, telegramConnected) |
PUT /me/profile → PATCH | Сменить метод |
GET /me/profile | Расширить response (children/interests, accountStatus) |
7. Новые модели БД (сводка)
| Модель | Описание |
|---|---|
| ParentStudentLink | Связь родитель ↔ ребёнок (заменяет Student.parentId) |
| StudentSubjectLink | Интересы студента (many-to-many с subject_registry) |
| SellerAddress | Физические адреса продавца (с координатами) |
| SellerSubjectLink | Направления продавца (many-to-many с subject_registry) |
| PasswordResetToken | Токены сброса пароля (email + SMS) |
| TelegramVerificationCode | Коды верификации Telegram |
8. Новые enum'ы
| Enum | Значения |
|---|---|
| WorkFormat | ONLINE, OFFLINE, MOBILE_TUTOR |
| ResetType | EMAIL_LINK, SMS_CODE |
9. Приоритеты реализации
Выполнено на backend
- Добавлены
firstName/lastNameнаAccount Account.emailсделан nullable- Реализованы
POST /auth/register/sellerиPOST /auth/register/buyer - Реализован
POST /auth/check-availability - Расширены seller и buyer profile contracts
- Реализованы
SellerAddress, address CRUD и primary switch - Реализованы
SellerSubjectLinkиStudentSubjectLink - Работают специфичные ошибки
EMAIL_TAKEN/PHONE_TAKEN - Валидация пароля требует минимум 8 символов и минимум 1 цифру
- Реализованы password reset endpoints
- Реализованы children CRUD и
PATCH /me/interests - Реализованы
PATCH-контракты для buyer/seller profile при сохранённыхPUTaliases - Реализован
GET /subjects - Реализован
POST /upload/image AccountStatusпереведён наACTIVE | UNDER_REVIEW | BLOCKED- Реализован
POST /auth/add-buyer-role - Реализована seller Telegram verification
- Реализован
PATCH /admin/sellers/:id/status - На auth endpoints включён throttling на уровне API
- Seller profile contract теперь возвращает специфичные
PHONE_TAKEN/EMAIL_TAKENи на update-flow POST /seller/telegram/verifyвозвращаетusernameпри наличии его в verification payload
Остаточный техдолг после реализации
- Подключить реальный SMS/email provider вместо stub transport
- Перевести upload storage с локального диска на S3/CDN-совместимый backend
- Дочистить Prisma schema от legacy compatibility полей и naming drift
- Довести
qadam-web, который ведёт отдельная frontend-команда, до полного использования нового registration/profile flow через OpenAPI contract
10. Открытые вопросы для команды
| # | Вопрос | Контекст |
|---|---|---|
| 1 | Когда удалять legacy POST /auth/register и PUT aliases? | Сейчас они сохранены ради обратной совместимости. Удалять только после подтверждённого cutover всех клиентов на новый контракт. |
| 2 | Какой провайдер использовать для SMS/email reset flow? | Backend уже создаёт токены и коды, но транспорт пока stub-овый. |
| 3 | Какой storage/CDN использовать для image uploads? | Текущая реализация локальная и production-safe для MVP, но не финальная для масштабирования. |
| 4 | Когда вычищать legacy Prisma-поля? | Нужен отдельный migration пакет после того, как frontend и админские сценарии перестанут зависеть от compatibility naming. |