Qadam Roadmap
проектdocs/product/requirements-api-registration.md

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 artifact apps/api/openapi/openapi.json и production URL https://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/registerlegacy route, сохранён для обратной совместимости
PUT/api/v1/seller/profilelegacy alias для PATCH /api/v1/seller/profile
PUT/api/v1/me/profilelegacy 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 }] }

Что создаётся на сервере (транзакция):

  1. Account (firstName, lastName, phone, email, passwordHash, type=SELLER, status=ACTIVE)
  2. Seller (sellerType)
  3. SchoolProfile / OnlineSchoolProfile / IndividualContributorProfile (в зависимости от sellerType)
  4. SellerAddress[] (если переданы)
  5. SellerSubjectLink[] (по subjectIds)
  6. 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 }] }

Что создаётся на сервере (транзакция):

  1. Account (firstName, lastName, phone, email?, passwordHash, type=BUYER, status=ACTIVE)
  2. Buyer (buyerType)
  3. Parent (firstName, lastName, phone, email) — если buyerType=PARENT
  4. Student[] + ParentStudentLink[] — если переданы children
  5. Student (firstName, lastName, birthYear, schoolGrade) — если buyerType=STUDENT
  6. StudentSubjectLink[] — если переданы subjectIds
  7. 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Описание
1POST/api/auth/register/sellerPublicРегистрация продавца (единый)
2POST/api/auth/register/buyerPublicРегистрация байера (единый)
3POST/api/auth/check-availabilityPublicПроверка email/phone
4POST/api/auth/forgot-passwordPublicНачать восстановление пароля
5POST/api/auth/verify-reset-codePublicПроверить SMS-код
6POST/api/auth/reset-passwordPublicУстановить новый пароль
7POST/api/auth/add-buyer-roleSellerДобавить buyer-роль
8GET/api/me/childrenBuyer(parent)Список детей
9POST/api/me/childrenBuyer(parent)Добавить ребёнка
10PATCH/api/me/children/:idBuyer(parent)Обновить ребёнка
11DELETE/api/me/children/:idBuyer(parent)Удалить ребёнка
12PATCH/api/me/interestsBuyer(student)Обновить интересы
13GET/api/seller/addressesSellerСписок адресов
14POST/api/seller/addressesSellerДобавить адрес
15PATCH/api/seller/addresses/:idSellerОбновить адрес
16DELETE/api/seller/addresses/:idSellerУдалить адрес
17PATCH/api/seller/addresses/:id/set-primarySellerСделать основным
18POST/api/seller/telegram/verifySellerВерифицировать Telegram
19DELETE/api/seller/telegramSellerОтключить Telegram
20PATCH/api/admin/sellers/:id/statusAdminИзменить статус
21GET/api/subjectsPublicСправочник направлений
22POST/api/upload/imageAuthЗагрузка изображения

6. Изменения существующих эндпоинтов

ЭндпоинтЧто изменить
POST /auth/registerОставить для обратной совместимости или удалить. Новые: /auth/register/seller и /auth/register/buyer
POST /seller/profileРасширить DTO (shortDesc, fullDesc, subjects, socials, addresses, workFormat)
PUT /seller/profilePATCHСменить метод, расширить DTO
GET /seller/profileРасширить response (subjects, addresses, socials, telegramConnected)
PUT /me/profilePATCHСменить метод
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Значения
WorkFormatONLINE, OFFLINE, MOBILE_TUTOR
ResetTypeEMAIL_LINK, SMS_CODE

9. Приоритеты реализации

Выполнено на backend

  1. Добавлены firstName/lastName на Account
  2. Account.email сделан nullable
  3. Реализованы POST /auth/register/seller и POST /auth/register/buyer
  4. Реализован POST /auth/check-availability
  5. Расширены seller и buyer profile contracts
  6. Реализованы SellerAddress, address CRUD и primary switch
  7. Реализованы SellerSubjectLink и StudentSubjectLink
  8. Работают специфичные ошибки EMAIL_TAKEN / PHONE_TAKEN
  9. Валидация пароля требует минимум 8 символов и минимум 1 цифру
  10. Реализованы password reset endpoints
  11. Реализованы children CRUD и PATCH /me/interests
  12. Реализованы PATCH-контракты для buyer/seller profile при сохранённых PUT aliases
  13. Реализован GET /subjects
  14. Реализован POST /upload/image
  15. AccountStatus переведён на ACTIVE | UNDER_REVIEW | BLOCKED
  16. Реализован POST /auth/add-buyer-role
  17. Реализована seller Telegram verification
  18. Реализован PATCH /admin/sellers/:id/status
  19. На auth endpoints включён throttling на уровне API
  20. Seller profile contract теперь возвращает специфичные PHONE_TAKEN / EMAIL_TAKEN и на update-flow
  21. POST /seller/telegram/verify возвращает username при наличии его в verification payload

Остаточный техдолг после реализации

  1. Подключить реальный SMS/email provider вместо stub transport
  2. Перевести upload storage с локального диска на S3/CDN-совместимый backend
  3. Дочистить Prisma schema от legacy compatibility полей и naming drift
  4. Довести 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.