# API Requirements — Регистрация (Buyer + Seller)

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

- Статус документа: target requirements
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении целевых продуктовых требований, backend-контракта или registration flow
- Область применения: целевые требования к крупным прикладным backend/API потокам
- Связанные документы:
  - [Карта API-маршрутов](../architecture/api-routes.md)
  - [Change Log для frontend-команды](../frontend/frontend-change-log.md)
  - [Roadmap](../project/roadmap.md)

> Источники: 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/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-регистрации

```typescript
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 — новые поля

```diff
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

```diff
enum AccountStatus {
  ACTIVE
- SUSPENDED
- DELETED
+ UNDER_REVIEW
+ BLOCKED
}
```

Закрыто: enum уже мигрирован в Prisma и production БД. Исторические значения `SUSPENDED` и `DELETED` были замапплены в `BLOCKED` во время миграции `20260328140000_registration_flow_upgrade`.

### 2.3 Parent — разделить name на firstName + lastName

```diff
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 — изменения полей

```diff
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

```prisma
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

```prisma
model StudentSubjectLink {
  id        String @id @default(uuid())
  studentId String
  subjectId String

  student Student @relation(fields: [studentId], references: [id])

  @@unique([studentId, subjectId])
}
```

### 2.7 SchoolProfile — расширение полей

```diff
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 — расширение полей

```diff
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

```prisma
enum WorkFormat {
  ONLINE
  OFFLINE
  MOBILE_TUTOR
}
```

### 2.11 Seller — новые поля

```diff
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

```prisma
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

```prisma
model SellerSubjectLink {
  id        String @id @default(uuid())
  sellerId  String
  subjectId String

  seller Seller @relation(fields: [sellerId], references: [id])

  @@unique([sellerId, subjectId])
}
```

### 2.14 Новая модель: PasswordResetToken

```prisma
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

```prisma
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 при нажатии "Продолжить", **ДО финальной регистрации**.

```typescript
// 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). Один запрос создаёт всё.

```typescript
// 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` — Регистрация байера (единый)

```typescript
// 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`

```typescript
// 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 — ввод кода).

```typescript
// 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`

Финальный шаг — установка нового пароля.

```typescript
// 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 — добавить профиль покупателя.

```typescript
// 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

```typescript
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)

```typescript
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`

```typescript
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`

```typescript
// 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`

```typescript
// 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):

```typescript
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)

```typescript
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

```typescript
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

```typescript
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)
```

```typescript
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
```

Публичный (без авторизации). Возвращает список направлений для мультиселекта.

```typescript
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
```

```typescript
// 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
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. |
