# FitCRM -- Документация для разработчиков

CRM-система для фитнес-клубов с шестью ролями пользователей, мобильным приложением, веб-панелями и встроенной SIP-телефонией.

---

## Содержание

1. [Архитектура проекта](#1-архитектура-проекта)
2. [Структура монорепозитория](#2-структура-монорепозитория)
3. [Быстрый старт](#3-быстрый-старт)
4. [Переменные окружения](#4-переменные-окружения)
5. [База данных (Prisma)](#5-база-данных-prisma)
6. [Аутентификация и авторизация](#6-аутентификация-и-авторизация)
7. [Multi-tenancy (RLS)](#7-multi-tenancy-rls)
8. [Справочник API](#8-справочник-api)
   - [Auth](#81-auth-v1auth)
   - [Users](#82-users-v1users)
   - [Clients](#83-clients-v1clients)
   - [Trainings](#84-trainings-v1trainings)
   - [Funnel](#85-funnel-v1funnel)
   - [Sales](#86-sales-v1sales)
   - [Stats](#87-stats-v1stats)
   - [Notifications](#88-notifications-v1notifications)
   - [SIP](#89-sip-v1sip)
   - [Work Schedule](#810-work-schedule-v1work-schedule)
   - [Modules](#811-modules-v1modules)
   - [Metering](#812-metering-v1metering)
   - [Provisioning](#813-provisioning-v1clubs)
   - [Webhooks API](#814-webhooks-api-v1webhooks)
   - [Catalog Services](#815-catalog--services-v1catalogservices)
   - [Catalog Packages](#816-catalog--packages-v1catalogpackages)
   - [Departments](#817-departments-v1departments)
   - [Rooms](#818-rooms-v1rooms)
   - [Clubs Admin](#819-clubs-admin-v1adminclubs)
   - [Licenses](#820-licenses-v1licenses)
   - [Reports](#821-reports-v1reports)
   - [Integration 1C](#822-integration-1c-v1integration)
   - [Audit Logs](#823-audit-logs-v1adminaudit-logs)
9. [Пагинация](#9-пагинация)
10. [Обработка ошибок](#10-обработка-ошибок)
11. [Модульная система](#11-модульная-система)
12. [Роли и матрица доступа](#12-роли-и-матрица-доступа)
13. [Webhook-события](#13-webhook-события)
14. [Push-уведомления](#14-push-уведомления)
15. [SIP-телефония](#15-sip-телефония)
16. [Конвенции кода](#16-конвенции-кода)
17. [Тестирование](#17-тестирование)
18. [Docker и инфраструктура](#18-docker-и-инфраструктура)
19. [Swagger / OpenAPI](#19-swagger--openapi)
20. [Очереди задач (BullMQ)](#20-очереди-задач-bullmq)
21. [Rate Limiting](#21-rate-limiting)

---

## 1. Архитектура проекта

### Общая схема

```
                            +-------------------+
                            |  Mobile App       |
                            |  React Native     |
                            +--------+----------+
                                     |
                                     v
+------------------+        +--------+----------+        +------------------+
| Web Admin        | -----> |                   | <----- | Web Club Admin   |
| (Reception)      |        |  NestJS API       |        | (Клуб-админ)     |
| :3001            |        |  :3000            |        | :3002            |
+------------------+        |                   |        +------------------+
                            |  /v1/*            |
+------------------+        |                   |        +------------------+
| Web Platform     | -----> |  JWT + RLS        | <----> | PostgreSQL 17    |
| Admin            |        |  Multi-tenancy    |        | + RLS policies   |
| :3003            |        |                   |        +------------------+
+------------------+        +---+----------+----+
                                |          |
                                v          v
                        +-------+--+  +----+------+
                        | Redis 8  |  | coturn    |
                        | (cache,  |  | STUN/TURN |
                        |  queues) |  +----------+
                        +----------+
```

### Технологический стек

| Слой | Технология | Версия |
|------|-----------|--------|
| Backend | NestJS | 11 |
| ORM | Prisma | 6 |
| БД | PostgreSQL | 17 |
| Кэш / Очереди | Redis + BullMQ | 8 |
| Mobile | React Native | 0.76+ |
| Стейт (Mobile) | Zustand + React Query | -- |
| Web | Next.js (App Router) | 15 |
| UI (Web) | shadcn/ui + Tailwind CSS | 4 |
| Монорепо | Turborepo + pnpm | 2.3+ / 9.14 |
| Язык | TypeScript (strict mode) | 5.5+ |
| Runtime | Node.js | 22 LTS |

---

## 2. Структура монорепозитория

```
fitcrm/
├── apps/
│   ├── api/                        # NestJS backend (порт 3000)
│   │   ├── prisma/
│   │   │   └── schema.prisma       # 34 модели, 12 перечислений
│   │   ├── src/
│   │   │   ├── common/             # Guards, middleware, decorators, interceptors
│   │   │   │   ├── decorators/     # @Roles, @CurrentUser, @RequireModule
│   │   │   │   ├── guards/         # ModuleGuard
│   │   │   │   ├── interceptors/   # AuditLogInterceptor
│   │   │   │   ├── interfaces/     # TenantStrategy
│   │   │   │   ├── middleware/     # TenantMiddleware
│   │   │   │   └── strategies/     # RlsTenantStrategy
│   │   │   ├── config/             # configuration.ts -- env-переменные
│   │   │   ├── modules/
│   │   │   │   ├── auth/           # Аутентификация: JWT, API-ключи
│   │   │   │   ├── users/          # Пользователи (6 ролей)
│   │   │   │   ├── clients/        # Клиенты: CRUD, цели, травмы
│   │   │   │   ├── schedule/       # Расписание: тренировки, слоты
│   │   │   │   ├── funnel/         # Воронка продаж
│   │   │   │   ├── sales/          # Продажи и долги
│   │   │   │   ├── stats/          # Статистика и рейтинг
│   │   │   │   ├── notifications/  # Push-уведомления (9 типов)
│   │   │   │   ├── sip/            # SIP-телефония
│   │   │   │   ├── work-schedule/  # График работы тренеров
│   │   │   │   ├── modules/        # Модульная система (feature flags)
│   │   │   │   ├── metering/       # Usage metering
│   │   │   │   └── provisioning/   # Управление клубами
│   │   │   ├── prisma/             # PrismaService
│   │   │   ├── app.module.ts
│   │   │   └── main.ts
│   │   └── package.json
│   │
│   ├── mobile/                     # React Native приложение
│   ├── web-admin/                  # Панель рецепции (порт 3001)
│   ├── web-club-admin/             # Панель админа клуба (порт 3002)
│   └── web-platform-admin/         # Суперадмин платформы (порт 3003)
│
├── packages/
│   ├── shared-types/               # Общие TypeScript типы
│   ├── api-client/                 # Генерируемый API-клиент (OpenAPI)
│   └── validators/                 # Общие Zod-схемы валидации
│
├── infra/
│   └── docker/
│       ├── docker-compose.yml      # PostgreSQL + Redis + coturn
│       └── docker-compose.prod.yml # Production-стек
│
├── docs/
│   ├── tz/                         # Техническое задание
│   ├── plan/                       # План разработки
│   └── dev/                        # Документация для разработчиков (этот файл)
│
├── package.json                    # Корень монорепо (pnpm workspaces)
├── pnpm-workspace.yaml
├── turbo.json
└── CLAUDE.md                       # Проектные инструкции
```

---

## 3. Быстрый старт

### Требования

- Node.js >= 22.0.0
- pnpm >= 9.14.0
- Docker + Docker Compose
- PostgreSQL 17 (через Docker)
- Redis 8 (через Docker)

### Установка и запуск

```bash
# 1. Клонирование репозитория
git clone <repo-url> fitcrm
cd fitcrm

# 2. Установка зависимостей
pnpm install

# 3. Запуск инфраструктуры (PostgreSQL, Redis, coturn)
cd infra/docker
docker compose up -d
cd ../..

# 4. Настройка переменных окружения
cp .env.example .env
# Отредактировать .env (см. раздел "Переменные окружения")

# 5. Инициализация базы данных
pnpm --filter api prisma:migrate
pnpm --filter api prisma:generate
pnpm --filter api prisma:seed       # Опциональная заливка тестовых данных

# 6. Запуск в dev-режиме
pnpm dev                             # Все сервисы одновременно
```

### Запуск отдельных приложений

```bash
pnpm --filter api dev                # Backend API (порт 3000)
pnpm --filter mobile start           # React Native
pnpm --filter web-admin dev          # Панель рецепции (порт 3001)
pnpm --filter web-club-admin dev     # Панель админа клуба (порт 3002)
pnpm --filter web-platform-admin dev # Суперадмин (порт 3003)
```

### Работа с базой данных

```bash
pnpm --filter api prisma:migrate     # Применить миграции
pnpm --filter api prisma:generate    # Пересоздать Prisma Client
pnpm --filter api prisma:seed        # Заполнить тестовыми данными
pnpm --filter api prisma:studio      # Открыть Prisma Studio (GUI)
```

### Сборка

```bash
pnpm build                           # Сборка всех приложений
pnpm lint                            # Линтинг
pnpm format                          # Форматирование (Prettier)
pnpm test                            # Запуск тестов
```

---

## 4. Переменные окружения

Файл `.env` в корне проекта. Переменные используются как Docker Compose, так и API-сервером.

| Переменная | По умолчанию | Описание |
|-----------|-------------|---------|
| `PORT` | `3000` | Порт API-сервера |
| `DATABASE_URL` | `postgresql://fitcrm:fitcrm@localhost:5432/fitcrm` | Строка подключения к PostgreSQL |
| `JWT_SECRET` | -- (обязательно) | Секрет для подписи access-токенов |
| `JWT_REFRESH_SECRET` | -- (обязательно) | Секрет для подписи refresh-токенов |
| `JWT_EXPIRES_IN` | `15m` | Время жизни access-токена |
| `JWT_REFRESH_EXPIRES_IN` | `30d` | Время жизни refresh-токена |
| `REDIS_HOST` | `localhost` | Хост Redis |
| `REDIS_PORT` | `6379` | Порт Redis |
| `REDIS_PASSWORD` | -- | Пароль Redis |
| `S3_ENDPOINT` | -- | URL S3-совместимого хранилища |
| `S3_BUCKET` | `fitcrm` | Имя бакета |
| `S3_ACCESS_KEY` | -- | Ключ доступа S3 |
| `S3_SECRET_KEY` | -- | Секретный ключ S3 |
| `S3_REGION` | `us-east-1` | Регион S3 |
| `CORS_ORIGIN` | `*` | Разрешенные CORS-источники |
| `POSTGRES_USER` | -- | Пользователь PostgreSQL (для Docker) |
| `POSTGRES_PASSWORD` | -- | Пароль PostgreSQL (для Docker) |
| `POSTGRES_DB` | -- | Имя базы данных (для Docker) |
| `TURN_REALM` | -- | Realm для TURN-сервера |
| `TURN_SECRET` | -- | Секрет для TURN-сервера |

---

## 5. База данных (Prisma)

### Модели данных (34 модели)

Полная Prisma-схема расположена в `apps/api/prisma/schema.prisma`.

#### Основные модели

| Модель | Таблица | Описание |
|--------|---------|---------|
| `Club` | `clubs` | Фитнес-клуб (тенант) |
| `User` | `users` | Пользователь (6 ролей) |
| `RefreshToken` | `refresh_tokens` | Refresh-токены для ротации |
| `ApiKey` | `api_keys` | API-ключи для серверных интеграций |
| `Department` | `departments` | Департаменты клуба |
| `Room` | `rooms` | Залы и локации |
| `Service` | `services` | Услуги (типы тренировок) |
| `ServiceCategory` | `service_categories` | Категории услуг |
| `Package` | `packages` | Пакеты услуг |
| `Client` | `clients` | Клиенты клуба |
| `ClientMembership` | `client_memberships` | Членства клиентов |
| `ClientGoal` | `client_goals` | Цели клиента |
| `ClientInjury` | `client_injuries` | Травмы клиента |
| `ClientService` | `client_services` | Купленные услуги |
| `Training` | `trainings` | Тренировки |
| `FunnelEntry` | `funnel_entries` | Записи воронки продаж |
| `Sale` | `sales` | Продажи |
| `WorkSchedule` | `work_schedules` | График работы тренеров |
| `SipCall` | `sip_calls` | Журнал SIP-звонков |
| `SipConfig` | `sip_configs` | Конфигурация SIP (per-club) |
| `SipAccount` | `sip_accounts` | SIP-аккаунты (per-user) |
| `Notification` | `notifications` | Уведомления |
| `PushToken` | `push_tokens` | Push-токены устройств |
| `WebhookSubscription` | `webhook_subscriptions` | Подписки на webhook-события |
| `WebhookLog` | `webhook_logs` | Журнал доставки webhook |
| `ClubModule` | `club_modules` | Модули клуба (feature flags) |
| `License` | `licenses` | Лицензии клубов |
| `UsageRecord` | `usage_records` | Записи потребления ресурсов |
| `AuditLog` | `audit_logs` | Аудит-лог действий |
| `IntegrationConfig` | `integration_configs` | Настройки интеграций (1С) |
| `SyncLog` | `sync_logs` | Журнал синхронизации |
| `NotificationSetting` | `notification_settings` | Настройки уведомлений per-club |
| `Report` | `reports` | Генерируемые отчёты (PDF) |
| `RatingPeriod` | `rating_periods` | Снимки рейтингов тренеров |

#### Перечисления (12 enums)

| Enum | Значения |
|------|---------|
| `UserRole` | `TRAINER`, `COORDINATOR`, `MANAGER`, `RECEPTIONIST`, `CLUB_ADMIN`, `SUPER_ADMIN` |
| `Gender` | `MALE`, `FEMALE` |
| `TrainingStatus` | `PLANNED`, `CONFIRMED`, `COMPLETED`, `PAID`, `NOT_PAID`, `NO_SHOW`, `CANCELLED` |
| `CancelReason` | `CLIENT_CANCEL`, `NO_DEDUCTION`, `SALE_RETURN`, `SICK` |
| `FunnelStage` | `NEW`, `ASSIGNED`, `APPOINTMENT_SET`, `COMPLETED`, `REGULAR`, `TRANSFERRED`, `REFUSED` |
| `SaleStatus` | `PENDING`, `PAID`, `CANCELLED`, `REFUNDED` |
| `CallDirection` | `OUTBOUND` |
| `CallStatus` | `INITIATED`, `RINGING`, `ANSWERED`, `COMPLETED`, `FAILED`, `NO_ANSWER`, `BUSY` |
| `NotificationType` | `CLIENT_ASSIGNED`, `SERVICE_BOOKED`, `SERVICE_PAID`, `PACKAGE_TOPUP`, `BOOKING_CANCELLED`, `TRAINING_DEDUCTED`, `TRAININGS_ENDING`, `CLIENT_RETURNED`, `CLIENT_BIRTHDAY` |
| `LicenseType` | `STARTER`, `STANDARD`, `PREMIUM` |
| `ReportStatus` | `PENDING`, `GENERATING`, `COMPLETED`, `FAILED` |
| `SipTransport` | `UDP`, `TCP`, `TLS` |

#### Важные соглашения по схеме

- Каждая таблица (кроме `clubs`) содержит колонку `club_id` для multi-tenancy.
- UUID используется для всех первичных ключей (`@id @default(uuid())`).
- `createdAt` / `updatedAt` -- стандартные таймстемпы.
- Индексы по `club_id` на каждой таблице.
- Составные уникальные ключи: `[clubId, phone]` для пользователей, `[clubId, moduleId]` для модулей.

---

## 6. Аутентификация и авторизация

### Схема аутентификации

Система использует JWT-токены (JSON Web Token) с двухуровневой схемой:

- **Access token** -- JWT, время жизни 15 минут
- **Refresh token** -- JWT, время жизни 30 дней, хранится в таблице `refresh_tokens`

### Структура JWT payload (TokenPayload)

```typescript
interface TokenPayload {
  sub: string;     // UUID пользователя
  clubId: string;  // UUID клуба (тенанта)
  role: string;    // Роль: trainer | coordinator | manager | receptionist | club_admin | super_admin
}
```

### Поток авторизации

#### 1. Логин

```
POST /v1/auth/login
Content-Type: application/json

{
  "phone": "+79001234567",
  "password": "securePass123"
}
```

Ответ (200 OK):

```json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

Ошибки:

```json
// 401 Unauthorized -- неверные учётные данные
{
  "statusCode": 401,
  "message": "Invalid credentials",
  "error": "Unauthorized"
}

// 401 Unauthorized -- деактивированный аккаунт
{
  "statusCode": 401,
  "message": "Account is deactivated",
  "error": "Unauthorized"
}
```

#### 2. Обновление access-токена

```
POST /v1/auth/refresh
Content-Type: application/json

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

Ответ (200 OK):

```json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

Ошибки:

```json
// 401 Unauthorized -- истёкший или отозванный refresh-токен
{
  "statusCode": 401,
  "message": "Refresh token has been revoked or expired",
  "error": "Unauthorized"
}
```

#### 3. Использование access-токена

Все защищённые эндпоинты требуют заголовок `Authorization`:

```
Authorization: Bearer <access_token>
```

### API-ключи

Для серверных интеграций поддерживаются API-ключи с заголовком:

```
X-API-Key: <api_key>
```

API-ключи привязаны к клубу и имеют scopes (области действия):

| Scope | Описание |
|-------|---------|
| `clients:read` | Чтение клиентов |
| `clients:write` | Создание/изменение клиентов |
| `trainings:read` | Чтение тренировок |
| `trainings:write` | Создание/изменение тренировок |
| `sales:read` | Чтение продаж |
| `sales:write` | Создание/изменение продаж |
| `funnel:read` | Чтение воронки |
| `funnel:write` | Изменение воронки |
| `stats:read` | Чтение статистики |
| `webhooks:manage` | Управление webhook-подписками |

### Декораторы авторизации в контроллерах

```typescript
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles, UserRole } from '../../common/decorators/roles.decorator';
import { CurrentUser, JwtPayload } from '../../common/decorators/current-user.decorator';

@Controller('example')
@UseGuards(JwtAuthGuard)           // Требуется JWT
export class ExampleController {

  @Get()
  @UseGuards(RolesGuard)           // Проверка ролей
  @Roles(UserRole.TRAINER, UserRole.COORDINATOR)  // Только эти роли
  async find(@CurrentUser() user: JwtPayload) {
    // user.sub     -- UUID пользователя
    // user.clubId  -- UUID клуба
    // user.role    -- роль
  }
}
```

---

## 7. Multi-tenancy (RLS)

### Принцип изоляции данных

FitCRM использует **Row-Level Security (RLS)** на уровне PostgreSQL для изоляции данных между клубами. Каждая таблица содержит колонку `club_id`, а RLS-политики автоматически фильтруют строки.

### Как это работает

1. **JWT** содержит `clubId` -- UUID клуба, к которому принадлежит пользователь.
2. **TenantMiddleware** перехватывает каждый запрос и устанавливает сессионную переменную PostgreSQL:
   ```sql
   SET app.club_id = '<club-uuid>';
   ```
3. **RLS-политики** на каждой таблице используют эту переменную:
   ```sql
   ALTER TABLE clients ENABLE ROW LEVEL SECURITY;
   CREATE POLICY club_isolation ON clients
     USING (club_id = current_setting('app.club_id')::uuid);
   ```

### TenantMiddleware (код)

```typescript
// apps/api/src/common/middleware/tenant.middleware.ts
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private readonly prisma: PrismaService) {}

  async use(req: Request, _res: Response, next: NextFunction): Promise<void> {
    const user = req.user as { clubId?: string } | undefined;
    if (user?.clubId) {
      await this.prisma.setTenantId(user.clubId);
    }
    next();
  }
}
```

### Маршруты без tenant-контекста

Аутентификационные маршруты (`/v1/auth/login`, `/v1/auth/refresh`) исключены из TenantMiddleware, так как пользователь ещё не аутентифицирован.

### Strategy Pattern для будущей миграции

Система спроектирована с абстракцией `TenantStrategy`, чтобы в будущем можно было перейти на DB-per-tenant или K8s namespace-per-tenant без изменения бизнес-логики:

```typescript
interface TenantStrategy {
  resolve(req: Request): string;
  applyIsolation(clubId: string): Promise<void>;
  provision(club: CreateClubDto): Promise<Club>;
  deprovision(clubId: string): Promise<void>;
}
```

В текущей реализации используется `RlsTenantStrategy`.

---

## 8. Справочник API

Все эндпоинты доступны по префиксу `/v1`. Swagger-документация доступна по адресу `http://localhost:3000/api/docs`.

### Общие заголовки для всех защищённых эндпоинтов

```
Authorization: Bearer <access_token>
Content-Type: application/json
```

---

### 8.1. Auth (`/v1/auth`)

Аутентификация. Не требует JWT.

| Метод | Путь | Описание |
|-------|------|---------|
| `POST` | `/v1/auth/login` | Вход по телефону и паролю |
| `POST` | `/v1/auth/refresh` | Обновление access-токена |
| `POST` | `/v1/auth/totp/setup` | Генерация TOTP-секрета и QR-кода (SUPER_ADMIN) |
| `POST` | `/v1/auth/totp/verify` | Верификация и активация TOTP |
| `POST` | `/v1/auth/totp/validate` | Валидация TOTP-кода при входе |

#### POST /v1/auth/login

Запрос:

```json
{
  "phone": "+79001234567",
  "password": "securePass123"
}
```

| Поле | Тип | Обязательно | Валидация |
|------|-----|-------------|-----------|
| `phone` | `string` | да | Непустая строка |
| `password` | `string` | да | Минимум 6 символов |

Ответ (200):

```json
{
  "accessToken": "eyJhbGci...",
  "refreshToken": "eyJhbGci..."
}
```

#### POST /v1/auth/refresh

Запрос:

```json
{
  "refreshToken": "eyJhbGci..."
}
```

Ответ (200):

```json
{
  "accessToken": "eyJhbGci..."
}
```

#### POST /v1/auth/totp/setup

Генерация TOTP-секрета для двухфакторной аутентификации. Доступно только для `SUPER_ADMIN`.

Заголовки: `Authorization: Bearer <token>`

Ответ (200):

```json
{
  "secret": "JBSWY3DPEHPK3PXP",
  "qrCodeUrl": "data:image/png;base64,..."
}
```

#### POST /v1/auth/totp/verify

Верификация TOTP-кода и активация 2FA. Передается код из приложения-аутентификатора.

Запрос:

```json
{
  "code": "123456"
}
```

Ответ (200):

```json
{
  "success": true
}
```

#### POST /v1/auth/totp/validate

Валидация TOTP-кода при входе (вызывается после login, если у пользователя включена 2FA).

Запрос:

```json
{
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "code": "123456"
}
```

Ответ (200):

```json
{
  "accessToken": "eyJhbGci...",
  "refreshToken": "eyJhbGci..."
}
```

---

### 8.2. Users (`/v1/users`)

Управление пользователями. Требует JWT.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/users/trainers` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Список тренеров текущего клуба |

#### GET /v1/users/trainers

Ответ (200):

```json
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "firstName": "Иван",
    "lastName": "Петров",
    "phone": "+79001234567",
    "role": "TRAINER",
    "departmentId": "...",
    "avatarUrl": null,
    "isActive": true
  }
]
```

---

### 8.3. Clients (`/v1/clients`)

CRUD клиентов. Требует JWT. Тренер автоматически видит только своих клиентов.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/clients` | Все (JWT) | Список клиентов (cursor-пагинация) |
| `GET` | `/v1/clients/sleeping` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Спящие клиенты (>30 дней без активности) |
| `GET` | `/v1/clients/:id` | Все (JWT) | Детали клиента |
| `POST` | `/v1/clients` | COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Создание клиента |
| `PATCH` | `/v1/clients/:id` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Обновление клиента |
| `DELETE` | `/v1/clients/:id` | MANAGER, CLUB_ADMIN | Удаление клиента |
| `POST` | `/v1/clients/:id/assign` | COORDINATOR, MANAGER | Назначить тренера |
| `GET` | `/v1/clients/:id/goals` | Все (JWT) | Список целей клиента |
| `POST` | `/v1/clients/:id/goals` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Добавить цель |
| `GET` | `/v1/clients/:id/injuries` | Все (JWT) | Список травм клиента |
| `POST` | `/v1/clients/:id/injuries` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Добавить травму |

#### GET /v1/clients -- параметры запроса

| Параметр | Тип | По умолчанию | Описание |
|----------|-----|-------------|---------|
| `cursor` | `uuid` | -- | Курсор для пагинации (UUID последнего элемента) |
| `limit` | `int` | `20` | Количество записей (1-100) |
| `search` | `string` | -- | Поиск по имени или телефону |
| `assignedTrainerId` | `uuid` | -- | Фильтр по тренеру (для TRAINER устанавливается автоматически) |
| `sortBy` | `enum` | `createdAt` | Поле сортировки: `createdAt`, `lastName`, `lastActivityAt` |
| `sortOrder` | `enum` | `desc` | Направление: `asc`, `desc` |

#### POST /v1/clients -- создание клиента

Запрос:

```json
{
  "firstName": "Иван",
  "lastName": "Петров",
  "patronymic": "Сергеевич",
  "phone": "+79001234567",
  "email": "ivan@example.com",
  "birthDate": "1990-05-15",
  "gender": "MALE",
  "notes": "Предпочитает утренние сессии",
  "source": "website",
  "assignedTrainerId": "550e8400-e29b-41d4-a716-446655440000"
}
```

| Поле | Тип | Обязательно | Валидация |
|------|-----|-------------|-----------|
| `firstName` | `string` | да | Непустая строка |
| `lastName` | `string` | да | Непустая строка |
| `patronymic` | `string` | нет | -- |
| `phone` | `string` | да | Паттерн `^\+?[0-9]{10,15}$` |
| `email` | `string` | нет | Валидный email |
| `birthDate` | `string` | нет | ISO 8601 дата |
| `gender` | `enum` | нет | `MALE` или `FEMALE` |
| `notes` | `string` | нет | -- |
| `source` | `string` | нет | -- |
| `assignedTrainerId` | `uuid` | нет | UUID тренера |

#### Автофильтрация по роли TRAINER

При вызове `GET /v1/clients` с ролью `TRAINER` система автоматически добавляет фильтр `assignedTrainerId = user.sub`, поэтому тренер видит только назначенных ему клиентов. Это поведение реализовано методом `applyTrainerFilter()`.

---

### 8.4. Trainings (`/v1/trainings`)

Расписание тренировок. Требует JWT. Тренер автоматически видит только свои тренировки.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/trainings` | Все (JWT) | Список тренировок (cursor-пагинация) |
| `GET` | `/v1/trainings/date/:date` | Все (JWT) | Тренировки на дату (YYYY-MM-DD) |
| `GET` | `/v1/trainings/slots/:trainerId/:date` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Доступные слоты тренера |
| `GET` | `/v1/trainings/:id` | Все (JWT) | Детали тренировки |
| `POST` | `/v1/trainings` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Создание тренировки |
| `PATCH` | `/v1/trainings/:id` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Обновление (только PLANNED) |
| `POST` | `/v1/trainings/:id/cancel` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Отмена с указанием причины |
| `POST` | `/v1/trainings/:id/reschedule` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Перенос на другую дату/время |
| `POST` | `/v1/trainings/:id/complete` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Отметить как проведённую |
| `POST` | `/v1/trainings/:id/confirm` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Подтвердить запланированную |

#### POST /v1/trainings -- создание тренировки

```json
{
  "clientId": "550e8400-e29b-41d4-a716-446655440000",
  "serviceId": "550e8400-e29b-41d4-a716-446655440001",
  "roomId": "550e8400-e29b-41d4-a716-446655440002",
  "scheduledAt": "2026-03-01T10:00:00.000Z",
  "duration": 60,
  "isRepeating": false,
  "repeatCount": null
}
```

| Поле | Тип | Обязательно | Валидация |
|------|-----|-------------|-----------|
| `clientId` | `uuid` | да | UUID клиента |
| `serviceId` | `uuid` | нет | UUID услуги |
| `roomId` | `uuid` | нет | UUID зала |
| `scheduledAt` | `string` | да | ISO 8601 дата-время |
| `duration` | `int` | нет (60) | 15-480 минут |
| `isRepeating` | `boolean` | нет (false) | Еженедельный повтор |
| `repeatCount` | `int` | нет | 1-52 повторений |

#### POST /v1/trainings/:id/cancel -- отмена

```json
{
  "reason": "CLIENT_CANCEL",
  "comment": "Клиент заболел"
}
```

Допустимые причины: `CLIENT_CANCEL`, `NO_DEDUCTION`, `SALE_RETURN`, `SICK`.

#### Статусы тренировок и переходы

```
PLANNED --> CONFIRMED --> COMPLETED
                      --> PAID
                      --> NOT_PAID
                      --> NO_SHOW
PLANNED --> CANCELLED (с любого статуса кроме COMPLETED/PAID)
```

---

### 8.5. Funnel (`/v1/funnel`)

Воронка продаж. Требует JWT. Тренер видит только своих клиентов.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/funnel` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Записи воронки по этапам |
| `GET` | `/v1/funnel/stats` | COORDINATOR, MANAGER, CLUB_ADMIN | Статистика конверсии |
| `GET` | `/v1/funnel/distribution` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Распределение по этапам |
| `GET` | `/v1/funnel/client/:clientId` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | История клиента в воронке |
| `POST` | `/v1/funnel/assign` | COORDINATOR, MANAGER, RECEPTIONIST | Назначить клиента тренеру |
| `POST` | `/v1/funnel/:clientId/move` | TRAINER, COORDINATOR, MANAGER | Перевести на следующий этап |
| `POST` | `/v1/funnel/:clientId/transfer` | COORDINATOR, MANAGER | Передать в другой департамент |
| `POST` | `/v1/funnel/:clientId/refuse` | TRAINER, COORDINATOR, MANAGER | Отказ (с комментарием) |

#### Этапы воронки

```
NEW --> ASSIGNED --> APPOINTMENT_SET --> COMPLETED --> REGULAR
                                                  \--> TRANSFERRED
                                                  \--> REFUSED
```

| Этап | Описание |
|------|---------|
| `NEW` | Новый клиент, ещё не распределён |
| `ASSIGNED` | Назначен тренер |
| `APPOINTMENT_SET` | Назначена стартовая тренировка |
| `COMPLETED` | Стартовая тренировка проведена |
| `REGULAR` | Стал постоянным клиентом |
| `TRANSFERRED` | Передан в другой департамент |
| `REFUSED` | Отказ (с обязательным комментарием) |

#### POST /v1/funnel/assign

```json
{
  "clientId": "550e8400-e29b-41d4-a716-446655440000",
  "trainerId": "550e8400-e29b-41d4-a716-446655440001"
}
```

#### POST /v1/funnel/:clientId/move

```json
{
  "stage": "APPOINTMENT_SET",
  "comment": "Записан на ВПТ 5 марта"
}
```

#### POST /v1/funnel/:clientId/refuse

```json
{
  "comment": "Клиент переехал в другой город"
}
```

#### Формулы конверсии

```
Новые -> Назначено   = (Назначено / Новые) * 100%
Назначено -> Проведено = (Проведено / Назначено) * 100%
Проведено -> Постоянные = (Постоянные / Проведено) * 100%
```

---

### 8.6. Sales (`/v1/sales`)

Продажи и долги. Требует JWT. Тренер видит только свои продажи.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/sales` | Все (JWT) | Список продаж (cursor-пагинация) |
| `GET` | `/v1/sales/debts` | Все (JWT) | Неоплаченные продажи |
| `GET` | `/v1/sales/debts/summary` | Все (JWT) | Сумма долгов |
| `GET` | `/v1/sales/:id` | Все (JWT) | Детали продажи |
| `POST` | `/v1/sales` | TRAINER, MANAGER, RECEPTIONIST, CLUB_ADMIN | Создание продажи |
| `PATCH` | `/v1/sales/:id` | TRAINER, MANAGER, RECEPTIONIST, CLUB_ADMIN | Обновление (только PENDING) |
| `POST` | `/v1/sales/:id/pay` | MANAGER, RECEPTIONIST, CLUB_ADMIN | Отметить как оплаченную |
| `POST` | `/v1/sales/:id/cancel` | TRAINER, MANAGER, CLUB_ADMIN | Отменить продажу |
| `POST` | `/v1/sales/:id/refund` | MANAGER, CLUB_ADMIN | Возврат |

#### POST /v1/sales -- создание продажи

```json
{
  "clientId": "550e8400-e29b-41d4-a716-446655440000",
  "serviceId": "550e8400-e29b-41d4-a716-446655440001",
  "packageId": "550e8400-e29b-41d4-a716-446655440002",
  "amount": 5000,
  "description": "Пакет персональных тренировок, 10 занятий"
}
```

| Поле | Тип | Обязательно | Валидация |
|------|-----|-------------|-----------|
| `clientId` | `uuid` | да | UUID клиента |
| `serviceId` | `uuid` | нет | UUID услуги |
| `packageId` | `uuid` | нет | UUID пакета |
| `amount` | `number` | да | >= 0, максимум 2 знака после запятой |
| `description` | `string` | нет | -- |

#### Статусы продаж

```
PENDING --> PAID
PENDING --> CANCELLED
PAID    --> REFUNDED
```

---

### 8.7. Stats (`/v1/stats`)

Статистика и рейтинг. Требует JWT.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/stats/summary` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Дашборд KPI |
| `GET` | `/v1/stats/funnel` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Конверсия воронки |
| `GET` | `/v1/stats/rating` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Рейтинг тренеров |
| `GET` | `/v1/stats/trainer/:trainerId` | TRAINER, COORDINATOR, MANAGER, RECEPTIONIST, CLUB_ADMIN | Детальная статистика тренера |

#### Параметры запроса

| Параметр | Тип | Описание |
|----------|-----|---------|
| `trainerId` | `uuid` | Фильтр по тренеру (TRAINER всегда видит только свои) |
| `dateFrom` | `string` | Начало периода (ISO date) |
| `dateTo` | `string` | Конец периода (ISO date) |
| `period` | `string` | Период рейтинга (для `/rating`) |

#### Автофильтрация

- **TRAINER** -- всегда видит только свою статистику. Параметр `trainerId` принудительно устанавливается в `user.sub`.
- **COORDINATOR, MANAGER, CLUB_ADMIN** -- могут фильтровать по конкретному тренеру или видеть общую статистику.
- **TRAINER** не может просматривать статистику другого тренера через `GET /v1/stats/trainer/:trainerId` -- вернётся `403 Forbidden`.

#### GET /v1/stats/summary -- пример ответа

```json
{
  "trainings": {
    "total": 150,
    "completed": 120,
    "cancelled": 10,
    "planned": 20
  },
  "clients": {
    "total": 85,
    "new": 12,
    "sleeping": 5
  },
  "sales": {
    "total": 45,
    "amount": 225000,
    "debts": 15000
  },
  "upcoming": [
    {
      "id": "...",
      "clientName": "Иван Петров",
      "scheduledAt": "2026-02-18T14:00:00.000Z",
      "duration": 60
    }
  ]
}
```

---

### 8.8. Notifications (`/v1/notifications`)

Push-уведомления. Требует JWT.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/notifications` | Все (JWT) | Список уведомлений текущего пользователя |
| `GET` | `/v1/notifications/unread-count` | Все (JWT) | Количество непрочитанных |
| `PATCH` | `/v1/notifications/:id/read` | Все (JWT) | Отметить прочитанным |
| `PATCH` | `/v1/notifications/read-all` | Все (JWT) | Отметить все прочитанными |
| `POST` | `/v1/notifications/push-token` | Все (JWT) | Зарегистрировать push-токен устройства |
| `DELETE` | `/v1/notifications/push-token/:deviceId` | Все (JWT) | Удалить push-токен |

#### POST /v1/notifications/push-token

```json
{
  "token": "fcm_token_here",
  "platform": "ANDROID",
  "deviceId": "device-unique-id"
}
```

---

### 8.9. SIP (`/v1/sip`)

SIP-телефония. Требует JWT + модуль `sip` должен быть включён для клуба (`@RequireModule('sip')`).

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/sip/calls` | TRAINER, COORDINATOR, MANAGER | Журнал звонков |
| `GET` | `/v1/sip/calls/stats` | TRAINER, COORDINATOR, MANAGER | Статистика звонков |
| `GET` | `/v1/sip/calls/:id` | TRAINER, COORDINATOR, MANAGER | Детали звонка |
| `POST` | `/v1/sip/calls` | TRAINER, COORDINATOR, MANAGER | Создать запись о звонке |
| `PATCH` | `/v1/sip/calls/:id/status` | TRAINER, COORDINATOR, MANAGER | Обновить статус звонка |
| `GET` | `/v1/sip/config` | CLUB_ADMIN | Получить SIP-конфигурацию клуба |
| `PUT` | `/v1/sip/config` | CLUB_ADMIN | Создать/обновить SIP-конфигурацию |
| `GET` | `/v1/sip/account` | TRAINER, COORDINATOR, MANAGER | Свой SIP-аккаунт |
| `PUT` | `/v1/sip/account` | TRAINER, COORDINATOR, MANAGER | Обновить display name |

#### Статусы звонков

```
INITIATED --> RINGING --> ANSWERED --> COMPLETED
                      --> FAILED
                      --> NO_ANSWER
                      --> BUSY
```

#### Ошибка при отключённом модуле

Если модуль `sip` не включён для клуба, все SIP-эндпоинты вернут:

```json
{
  "statusCode": 403,
  "message": "Module \"sip\" is not enabled for this club",
  "error": "Forbidden"
}
```

---

### 8.10. Work Schedule (`/v1/work-schedule`)

График работы тренеров. Требует JWT.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/work-schedule` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Список записей графика |
| `GET` | `/v1/work-schedule/:id` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Детали записи |
| `POST` | `/v1/work-schedule` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Создать запись |
| `POST` | `/v1/work-schedule/bulk` | COORDINATOR, MANAGER, CLUB_ADMIN | Массовое создание |
| `PATCH` | `/v1/work-schedule/:id` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Обновить запись |
| `DELETE` | `/v1/work-schedule/:id` | TRAINER, COORDINATOR, MANAGER, CLUB_ADMIN | Удалить запись |

#### Ограничения по ролям

- **TRAINER** -- видит и может изменять только свой график. При создании `trainerId` автоматически устанавливается из JWT.
- **COORDINATOR, MANAGER, CLUB_ADMIN** -- могут управлять графиком любого тренера, но обязаны указать `trainerId` в запросе.

#### POST /v1/work-schedule/bulk

```json
[
  {
    "trainerId": "550e8400-e29b-41d4-a716-446655440000",
    "date": "2026-03-01",
    "startTime": "09:00",
    "endTime": "18:00",
    "isAvailable": true
  },
  {
    "trainerId": "550e8400-e29b-41d4-a716-446655440000",
    "date": "2026-03-02",
    "startTime": "10:00",
    "endTime": "19:00",
    "isAvailable": true
  }
]
```

---

### 8.11. Modules (`/v1/modules`)

Модульная система (feature flags). Требует JWT.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/modules` | Все (JWT) | Все модули текущего клуба |
| `GET` | `/v1/modules/:moduleId` | Все (JWT) | Статус конкретного модуля |
| `PATCH` | `/v1/modules/:moduleId/enable` | CLUB_ADMIN | Включить модуль |
| `PATCH` | `/v1/modules/:moduleId/disable` | CLUB_ADMIN | Выключить модуль |

#### PATCH /v1/modules/:moduleId/enable

```json
{
  "limits": {
    "max_minutes": 1000
  }
}
```

Ответ:

```json
{
  "id": "...",
  "clubId": "...",
  "moduleId": "sip",
  "enabled": true,
  "limitsJson": { "max_minutes": 1000 },
  "usageJson": null
}
```

---

### 8.12. Metering (`/v1/metering`)

Мониторинг потребления ресурсов. Требует JWT.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `GET` | `/v1/metering/usage` | CLUB_ADMIN, SUPER_ADMIN | Текущее потребление |
| `GET` | `/v1/metering/limits` | CLUB_ADMIN, SUPER_ADMIN | Лимиты vs использование |

#### GET /v1/metering/usage -- пример ответа

```json
{
  "activeUsers": 8,
  "clients": 450,
  "callStorageGb": 1.25,
  "webhooksSent": 3200,
  "pushSent": 1500,
  "apiRequests": 25000
}
```

#### GET /v1/metering/limits -- пример ответа

```json
[
  {
    "resource": "activeUsers",
    "limit": 10,
    "used": 8,
    "percentage": 80,
    "exceeded": false
  },
  {
    "resource": "clients",
    "limit": 1000,
    "used": 450,
    "percentage": 45,
    "exceeded": false
  }
]
```

---

### 8.13. Provisioning (`/v1/clubs`)

Управление клубами. Только для SUPER_ADMIN.

| Метод | Путь | Роли | Описание |
|-------|------|------|---------|
| `POST` | `/v1/clubs` | SUPER_ADMIN | Создание нового клуба |
| `DELETE` | `/v1/clubs/:id` | SUPER_ADMIN | Деактивация клуба |

#### POST /v1/clubs

```json
{
  "name": "FitLife Москва",
  "slug": "fitlife-moscow",
  "address": "Москва, ул. Пушкина, 10",
  "phone": "+74951234567",
  "email": "info@fitlife.ru",
  "timezone": "Europe/Moscow"
}
```

Ответ (201):

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "FitLife Москва",
  "slug": "fitlife-moscow",
  "isActive": true,
  "createdAt": "2026-02-18T12:00:00.000Z"
}
```

---

### 8.14. Webhooks API (`/v1/webhooks`)

Управление webhook-подписками. Требует роль `CLUB_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/webhooks` | Список подписок клуба |
| `POST` | `/v1/webhooks` | Создать подписку |
| `GET` | `/v1/webhooks/:id` | Получить подписку |
| `PATCH` | `/v1/webhooks/:id` | Обновить подписку |
| `DELETE` | `/v1/webhooks/:id` | Удалить подписку |
| `GET` | `/v1/webhooks/:id/logs` | Журнал доставки |
| `POST` | `/v1/webhooks/:id/test` | Тестовая отправка |

#### POST /v1/webhooks

```json
{
  "url": "https://example.com/webhook",
  "events": ["client.created", "training.completed"],
  "secret": "my-webhook-secret",
  "isActive": true
}
```

Доставка: BullMQ очередь `webhook-delivery`, 3 попытки (5с → 30с → 5мин), HMAC-SHA256 подпись в заголовке `X-Webhook-Signature`.

---

### 8.15. Catalog — Services (`/v1/catalog/services`)

Управление каталогом услуг. Требует роль `CLUB_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/catalog/categories` | Список категорий |
| `POST` | `/v1/catalog/categories` | Создать категорию |
| `PATCH` | `/v1/catalog/categories/:id` | Обновить категорию |
| `DELETE` | `/v1/catalog/categories/:id` | Удалить категорию |
| `GET` | `/v1/catalog/services` | Список услуг |
| `POST` | `/v1/catalog/services` | Создать услугу |
| `PATCH` | `/v1/catalog/services/:id` | Обновить услугу |
| `DELETE` | `/v1/catalog/services/:id` | Удалить услугу |

#### POST /v1/catalog/services

```json
{
  "name": "Персональная тренировка",
  "categoryId": "uuid",
  "duration": 60,
  "price": 3000,
  "isActive": true
}
```

---

### 8.16. Catalog — Packages (`/v1/catalog/packages`)

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/catalog/packages` | Список пакетов |
| `POST` | `/v1/catalog/packages` | Создать пакет |
| `PATCH` | `/v1/catalog/packages/:id` | Обновить пакет |
| `DELETE` | `/v1/catalog/packages/:id` | Удалить пакет |

#### POST /v1/catalog/packages

```json
{
  "name": "Абонемент 10 тренировок",
  "serviceId": "uuid",
  "sessionsCount": 10,
  "validDays": 90,
  "price": 25000,
  "isActive": true
}
```

---

### 8.17. Departments (`/v1/departments`)

Управление департаментами клуба. Требует роль `CLUB_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/departments` | Список департаментов |
| `POST` | `/v1/departments` | Создать департамент |
| `GET` | `/v1/departments/:id` | Получить департамент |
| `PATCH` | `/v1/departments/:id` | Обновить департамент |
| `DELETE` | `/v1/departments/:id` | Удалить департамент |

#### POST /v1/departments

```json
{
  "name": "Фитнес",
  "description": "Персональные и групповые тренировки"
}
```

---

### 8.18. Rooms (`/v1/rooms`)

Управление залами и локациями клуба. Требует роль `CLUB_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/rooms` | Список залов |
| `POST` | `/v1/rooms` | Создать зал |
| `GET` | `/v1/rooms/:id` | Получить зал |
| `PATCH` | `/v1/rooms/:id` | Обновить зал |
| `DELETE` | `/v1/rooms/:id` | Удалить зал |

#### POST /v1/rooms

```json
{
  "name": "Зал силовых тренировок",
  "capacity": 20,
  "equipment": "Штанги, гантели, тренажеры"
}
```

---

### 8.19. Clubs Admin (`/v1/admin/clubs`)

Управление клубами платформы. Требует роль `SUPER_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/admin/clubs` | Список всех клубов |
| `GET` | `/v1/admin/clubs/:id` | Получить клуб по ID |
| `PATCH` | `/v1/admin/clubs/:id` | Обновить настройки клуба |
| `PATCH` | `/v1/admin/clubs/:id/status` | Изменить статус клуба (активен/заблокирован) |

---

### 8.20. Licenses (`/v1/licenses`)

Управление лицензиями. Требует роль `SUPER_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/licenses` | Список лицензий |
| `POST` | `/v1/licenses` | Создать лицензию |
| `GET` | `/v1/licenses/:id` | Получить лицензию |
| `PATCH` | `/v1/licenses/:id` | Обновить лицензию |
| `POST` | `/v1/licenses/:id/renew` | Продлить лицензию |
| `GET` | `/v1/licenses/types` | Типы лицензий |
| `GET` | `/v1/licenses/club/:clubId` | Лицензия по клубу |

#### POST /v1/licenses

```json
{
  "clubId": "uuid",
  "type": "STANDARD",
  "validUntil": "2027-02-18T00:00:00.000Z",
  "modules": ["core", "sales", "sip", "webhooks"]
}
```

---

### 8.21. Reports (`/v1/reports`)

Генерация отчётов. Доступно ролям `MANAGER`, `RECEPTIONIST`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/reports/current-state` | Текущее состояние клуба |
| `GET` | `/v1/reports/statistics` | Статистика за период |
| `GET` | `/v1/reports/trainer-stats` | Статистика по тренерам |
| `POST` | `/v1/reports/generate` | Генерация PDF-отчёта |

---

### 8.22. Integration 1C (`/v1/integration`)

Настройки интеграции с 1С:Фитнес клуб. Требует роль `CLUB_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/integration/config` | Получить настройки 1С |
| `PATCH` | `/v1/integration/config` | Обновить настройки 1С |
| `POST` | `/v1/integration/sync` | Запустить синхронизацию |
| `GET` | `/v1/integration/sync/status` | Статус последней синхронизации |

---

### 8.23. Audit Logs (`/v1/admin/audit-logs`)

Журнал действий администраторов. Требует роль `SUPER_ADMIN`.

| Метод | Путь | Описание |
|-------|------|---------|
| `GET` | `/v1/admin/audit-logs` | Список записей аудита |

#### GET /v1/admin/audit-logs

Параметры запроса:

| Параметр | Тип | Описание |
|----------|-----|---------|
| `userId` | `uuid` | Фильтр по пользователю |
| `action` | `string` | Фильтр по действию (`CREATE`, `UPDATE`, `DELETE`, `LOGIN`) |
| `entity` | `string` | Фильтр по сущности (`club`, `license`, `user`) |
| `from` | `ISO 8601` | Начало периода |
| `to` | `ISO 8601` | Конец периода |
| `cursor` | `uuid` | Курсор пагинации |
| `limit` | `int` | Лимит (по умолчанию 50) |

Ответ (200):

```json
{
  "data": [
    {
      "id": "uuid",
      "userId": "uuid",
      "userName": "Иванов Иван",
      "action": "CREATE",
      "entity": "club",
      "entityId": "uuid",
      "details": { "name": "FitLife Москва" },
      "ipAddress": "192.168.1.1",
      "createdAt": "2026-02-18T12:00:00.000Z"
    }
  ],
  "meta": { "hasMore": false, "nextCursor": null, "total": 1 }
}
```

Данные доступны для экспорта в CSV из веб-панели суперадминистратора.

---

## 9. Пагинация

Все списочные эндпоинты используют **cursor-based пагинацию** (не offset).

### Параметры запроса

| Параметр | Тип | По умолчанию | Описание |
|----------|-----|-------------|---------|
| `cursor` | `uuid` | `null` | UUID последнего элемента предыдущей страницы |
| `limit` | `int` | `20` | Количество записей на страницу (1-100) |

### Формат ответа

```json
{
  "data": [
    { "id": "uuid-1", "..." : "..." },
    { "id": "uuid-2", "..." : "..." }
  ],
  "nextCursor": "uuid-2",
  "hasMore": true
}
```

### Пример использования

```
# Первая страница
GET /v1/clients?limit=20

# Вторая страница
GET /v1/clients?limit=20&cursor=uuid-20

# Третья страница
GET /v1/clients?limit=20&cursor=uuid-40
```

### Почему cursor, а не offset

- **Стабильность**: cursor-пагинация не теряет и не дублирует записи при вставках/удалениях между страницами.
- **Производительность**: нет `OFFSET N`, который сканирует N записей перед выдачей.
- **Масштабируемость**: работает одинаково быстро на 100-й и 10000-й странице.

---

## 10. Обработка ошибок

### Формат ошибок

Все ошибки возвращаются в стандартном формате NestJS:

```json
{
  "statusCode": 400,
  "message": "Описание ошибки",
  "error": "Bad Request"
}
```

Для ошибок валидации:

```json
{
  "statusCode": 400,
  "message": [
    "phone must be a valid number with 10-15 digits, optionally starting with +",
    "firstName should not be empty"
  ],
  "error": "Bad Request"
}
```

### HTTP-коды ответов

| Код | Описание | Когда используется |
|-----|---------|-------------------|
| `200` | OK | Успешный GET, PATCH, действия (pay, cancel, complete) |
| `201` | Created | Успешный POST (создание) |
| `204` | No Content | Успешный DELETE |
| `400` | Bad Request | Невалидные данные, некорректный переход статуса |
| `401` | Unauthorized | Невалидный или отсутствующий JWT / refresh-токен |
| `403` | Forbidden | Недостаточно прав (роль), модуль отключён |
| `404` | Not Found | Сущность не найдена |
| `409` | Conflict | Конфликт (например, slug клуба уже существует) |

### Валидация входных данных

API использует глобальный `ValidationPipe` со следующими настройками:

```typescript
new ValidationPipe({
  whitelist: true,          // Удалять неизвестные поля
  transform: true,          // Преобразовывать типы
  forbidNonWhitelisted: true, // Ошибка при неизвестных полях
  transformOptions: {
    enableImplicitConversion: true,
  },
})
```

Это означает:
- Неизвестные поля в теле запроса вызовут ошибку `400`.
- Строковые числа в query-параметрах автоматически преобразуются в числа.
- DTO используют декораторы `class-validator` (`@IsString`, `@IsUUID`, `@IsInt`, `@Min`, `@Max` и т.д.).

---

## 11. Модульная система

### Концепция

Функциональность системы разделена на модули, которые можно включать/выключать per-club через таблицу `club_modules`. Это реализует модель лицензирования -- базовая функциональность доступна всем, а продвинутые возможности (SIP, webhooks) требуют активации.

### Перечень модулей

| ID модуля | Название | Стартовая | Стандартная | Премиум |
|-----------|---------|-----------|------------|---------|
| `core` | Ядро CRM | включён | включён | включён |
| `sales` | Продажи и реализация | включён | включён | включён |
| `coordinator` | Координатор | включён | включён | включён |
| `push` | Push-уведомления | включён | включён | включён |
| `web_reception` | Веб: Рецепция | включён | включён | включён |
| `web_reports` | Веб: Отчёты | -- | включён | включён |
| `sip` | SIP-телефония | -- | включён | включён |
| `sip_recording` | Запись звонков | -- | -- | включён |
| `webhooks` | Webhooks + API | -- | включён | включён |
| `club_admin` | Панель админа клуба | -- | включён | включён |
| `1c_sync` | Интеграция с 1С | -- | включён | включён |

### Защита на уровне API

Контроллеры используют декоратор `@RequireModule()` и guard `ModuleGuard`:

```typescript
@Controller('v1/sip')
@UseGuards(JwtAuthGuard, ModuleGuard)
@RequireModule('sip')
export class SipController {
  // Все эндпоинты требуют включённого модуля 'sip'
}
```

`ModuleGuard` делает запрос к таблице `club_modules`:

```sql
SELECT enabled FROM club_modules WHERE club_id = $1 AND module_id = $2
```

Если модуль отключён или не найден, возвращается `403 Forbidden`.

### Защита на фронтенде

Для мобильных и веб-приложений используется хук:

```typescript
const { isEnabled, limits } = useModuleAccess('sip');
if (!isEnabled) return null; // Скрыть UI-элемент
```

---

## 12. Роли и матрица доступа

### Шесть ролей

| Роль | Enum | Платформа | Описание |
|------|------|-----------|---------|
| Тренер | `TRAINER` | Mobile | Работа с клиентами, тренировками, воронкой |
| Координатор | `COORDINATOR` | Mobile | Распределение клиентов, статистика департамента |
| Фитнес-менеджер | `MANAGER` | Mobile + Web | Полная аналитика, отчёты |
| Администратор рецепции | `RECEPTIONIST` | Web | Запись клиентов, распределение |
| Администратор клуба | `CLUB_ADMIN` | Web | Сотрудники, настройки, интеграции |
| Суперадминистратор | `SUPER_ADMIN` | Web | Управление клубами и лицензиями |

### Матрица доступа по эндпоинтам

| Эндпоинт | TRAINER | COORDINATOR | MANAGER | RECEPTIONIST | CLUB_ADMIN | SUPER_ADMIN |
|----------|---------|-------------|---------|-------------|------------|-------------|
| `GET /clients` | свои | все | все | все | все | -- |
| `POST /clients` | -- | да | да | да | да | -- |
| `DELETE /clients/:id` | -- | -- | да | -- | да | -- |
| `GET /trainings` | свои | все | все | все | все | -- |
| `POST /trainings` | да | да | да | да | да | -- |
| `GET /funnel` | свои | все | все | все | все | -- |
| `GET /funnel/stats` | -- | да | да | -- | да | -- |
| `POST /funnel/assign` | -- | да | да | да | -- | -- |
| `GET /sales` | свои | все | все | все | все | -- |
| `POST /sales` | да | -- | да | да | да | -- |
| `POST /sales/:id/pay` | -- | -- | да | да | да | -- |
| `POST /sales/:id/refund` | -- | -- | да | -- | да | -- |
| `GET /stats/*` | свои | все | все | все | все | -- |
| `GET /sip/calls` | свои | все | все | -- | -- | -- |
| `GET /sip/config` | -- | -- | -- | -- | да | -- |
| `POST /work-schedule/bulk` | -- | да | да | -- | да | -- |
| `PATCH /modules/*/enable` | -- | -- | -- | -- | да | -- |
| `GET /metering/*` | -- | -- | -- | -- | да | да |
| `POST /clubs` | -- | -- | -- | -- | -- | да |
| `DELETE /clubs/:id` | -- | -- | -- | -- | -- | да |

### Автоматическая фильтрация по роли

Для роли `TRAINER` система автоматически фильтрует данные. Это реализовано через метод `applyTrainerFilter()` в каждом контроллере:

```typescript
private applyTrainerFilter(user: JwtPayload, query: FindClientsDto): FindClientsDto {
  if (user.role === UserRole.TRAINER) {
    return { ...query, assignedTrainerId: user.sub };
  }
  return query;
}
```

Тренер **не может** обойти эту фильтрацию через query-параметры -- система принудительно устанавливает `trainerId` из JWT.

---

## 13. Webhook-события

### 12 типов исходящих событий

| Событие | Триггер |
|---------|---------|
| `client.created` | Создание клиента |
| `client.updated` | Обновление карточки клиента |
| `training.created` | Создание тренировки |
| `training.cancelled` | Отмена тренировки |
| `training.rescheduled` | Перенос тренировки |
| `training.completed` | Завершение тренировки |
| `sale.created` | Выставлена продажа |
| `sale.paid` | Продажа оплачена |
| `funnel.stage_changed` | Смена этапа воронки |
| `funnel.assigned` | Назначен тренер |
| `schedule.updated` | Изменён график работы |
| `membership.expiring` | Членство истекает (за 7 дней) |

### Формат webhook-запроса

```
POST <webhook_url>
Content-Type: application/json
X-FitCRM-Signature: sha256=<hmac_hex>
X-FitCRM-Event: client.created
X-FitCRM-Delivery: <uuid>

{
  "event": "client.created",
  "timestamp": "2026-02-18T12:00:00.000Z",
  "club_id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "id": "...",
    "firstName": "Иван",
    "lastName": "Петров",
    "phone": "+79001234567"
  },
  "signature": "sha256=abc123..."
}
```

### Подпись (HMAC-SHA256)

Подпись вычисляется как `HMAC-SHA256(webhook_secret, request_body)` и передаётся в заголовке `X-FitCRM-Signature`.

Проверка на стороне получателя (пример на Node.js):

```javascript
const crypto = require('crypto');

function verifySignature(secret, body, signature) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(body).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
```

### Retry-политика

При неуспешной доставке (HTTP ответ не 2xx или таймаут) система повторяет отправку:

| Попытка | Задержка |
|---------|---------|
| 1 | 5 секунд |
| 2 | 30 секунд |
| 3 | 5 минут |

После 3 неуспешных попыток доставка помечается как `failed`. Все попытки логируются в таблице `webhook_logs`.

---

## 14. Push-уведомления

### 9 типов уведомлений

| Тип | Триггер | Получатель |
|-----|---------|-----------|
| `CLIENT_ASSIGNED` | Новый клиент распределён тренеру | Тренер |
| `SERVICE_BOOKED` | Клиент записался на услугу | Тренер |
| `SERVICE_PAID` | Клиент оплатил услугу | Тренер |
| `PACKAGE_TOPUP` | Доплата пакета | Тренер |
| `BOOKING_CANCELLED` | Клиент отменил запись | Тренер |
| `TRAINING_DEDUCTED` | Списание тренировки | Тренер |
| `TRAININGS_ENDING` | Оставшиеся тренировки заканчиваются | Тренер |
| `CLIENT_RETURNED` | Возврат клиента от администрации | Тренер |
| `CLIENT_BIRTHDAY` | День рождения клиента | Тренер |

### Инфраструктура

- **Android**: Firebase Cloud Messaging (FCM)
- **iOS**: Apple Push Notification Service (APNs)

### Регистрация push-токена

```
POST /v1/notifications/push-token
{
  "token": "fcm_or_apns_token",
  "platform": "ANDROID",
  "deviceId": "unique-device-id"
}
```

### Deep linking

Каждое уведомление содержит поле `data` с JSON-объектом, который определяет навигацию в мобильном приложении при нажатии на уведомление.

### Настройка (Админ клуба)

Администратор клуба может через таблицу `notification_settings`:
- Включить/отключить каждый тип уведомления
- Задать кастомный текст для типов: `CLIENT_ASSIGNED`, `SERVICE_BOOKED`, `BOOKING_CANCELLED`, `TRAININGS_ENDING`

---

## 15. SIP-телефония

### Назначение

Встроенный SIP-клиент позволяет тренерам совершать исходящие звонки клиентам с CallerID клуба, исключая использование личного телефона.

### Технические параметры

| Параметр | Значение |
|----------|---------|
| Протокол | SIP/2.0 (RFC 3261) |
| Медиа | RTP/SRTP (RFC 3711) |
| Кодеки | G.711 a-law/u-law + Opus |
| Шифрование | SRTP (обязательно при TLS) |
| NAT traversal | STUN / TURN / ICE |
| Задержка | <= 200 мс |
| MOS | >= 3.5 |

### Архитектура

```
+------------------+     SIP/TLS     +------------------+
| Mobile App       | <-------------> | SIP Server       |
| (JsSIP)          |                 | (АТС клуба)     |
+------------------+                 +------------------+
        |
        | RTP/SRTP
        v
+------------------+
| coturn           |
| STUN/TURN Server |
+------------------+
```

### Конфигурация (per-club)

Таблица `sip_configs`:

```json
{
  "sipServer": "sip.example.com",
  "sipPort": 5060,
  "transport": "TLS",
  "callerId": "+74951234567",
  "codecs": ["opus", "g711_alaw", "g711_ulaw"],
  "stunServer": "stun:stun.example.com:3478",
  "turnServer": "turn:turn.example.com:3478",
  "turnUsername": "user",
  "turnPassword": "pass"
}
```

### Аккаунты (per-user)

Таблица `sip_accounts`:

```json
{
  "sipUsername": "trainer001",
  "sipPassword": "...",
  "displayName": "Иван Петров"
}
```

### Ограничения v1

- Только исходящие звонки (нет входящих)
- Без записи разговоров
- Без конференц-связи

### Требование модуля

Все SIP-эндпоинты защищены `@RequireModule('sip')`. Если модуль `sip` не включён в `club_modules`, доступ будет заблокирован с ошибкой `403`.

---

## 16. Конвенции кода

### Именование

| Категория | Стиль | Пример |
|-----------|-------|--------|
| Переменные, функции | camelCase | `findById`, `clientId` |
| Типы, интерфейсы, классы | PascalCase | `JwtPayload`, `CreateClientDto` |
| Файлы-утилиты | kebab-case | `date-helpers.ts` |
| Модули NestJS | kebab-case папка | `work-schedule/` |
| Enums | PascalCase (значения UPPER_CASE) | `UserRole.TRAINER` |

### Структура модуля NestJS

Один модуль = одна бизнес-сущность.

```
modules/clients/
├── dto/
│   ├── create-client.dto.ts
│   ├── update-client.dto.ts
│   ├── find-clients.dto.ts
│   └── index.ts
├── clients.controller.ts
├── clients.service.ts
├── clients.module.ts
└── index.ts
```

### Структура экрана React Native

```
screens/clients/
├── index.tsx           # Компонент экрана
├── hooks.ts            # Хуки, специфичные для экрана
└── components/
    ├── ClientCard.tsx
    └── ClientFilter.tsx
```

### DTO-валидация (Backend)

Все DTO используют `class-validator` + `class-transformer`:

```typescript
import { IsString, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateClientDto {
  @ApiProperty({ description: 'Client first name', example: 'Иван' })
  @IsString()
  @IsNotEmpty()
  firstName: string;

  @ApiPropertyOptional({ description: 'UUID of the trainer' })
  @IsUUID()
  @IsOptional()
  assignedTrainerId?: string;
}
```

### Swagger-аннотации

Каждый контроллер должен иметь:

```typescript
@ApiTags('Clients')           // Группировка в Swagger UI
@ApiBearerAuth()              // Требование авторизации
@Controller('clients')
export class ClientsController {

  @Get()
  @ApiOperation({ summary: 'List clients with pagination and filters' })
  @ApiResponse({ status: 200, description: 'Paginated list of clients' })
  @ApiResponse({ status: 401, description: 'Unauthorized' })
  async findAll() { ... }
}
```

### Ограничения

- **Максимум 300 строк** на файл
- **TypeScript strict mode** включён
- **UI на русском языке**
- **Тесты**: покрытие >= 70% бизнес-логики

---

## 17. Тестирование

### Стек тестирования

| Уровень | Инструмент | Место |
|---------|-----------|-------|
| Unit (Backend) | Jest + Supertest | `apps/api/src/**/*.spec.ts` |
| Unit (Mobile) | Jest + React Native Testing Library | `apps/mobile/src/**/*.test.ts` |
| Unit (Web) | Vitest + Testing Library | `apps/web-*/**/*.test.ts` |
| E2E (API) | Jest + Supertest | `apps/api/test/*.e2e-spec.ts` |
| E2E (Mobile) | Detox | -- |
| E2E (Web) | Playwright | -- |

### Запуск тестов

```bash
# Все тесты
pnpm test

# Backend unit-тесты
pnpm --filter api test

# Backend e2e-тесты
pnpm --filter api test:e2e

# Конкретный файл
pnpm --filter api test -- clients.service.spec.ts
```

### Критические тест-кейсы

1. **Multi-tenancy изоляция**: клуб A не видит данные клуба B.
2. **RBAC**: каждая роль имеет доступ только к разрешённым эндпоинтам.
3. **ModuleGuard**: отключённый модуль возвращает `403`.
4. **Автофильтрация TRAINER**: тренер видит только свои данные.
5. **Webhook HMAC**: подпись проверяется корректно.
6. **Лицензия grace period**: блокировка после истечения.
7. **Refresh token**: ротация, отзыв, истечение.
8. **Валидация DTO**: отклонение невалидных данных.

---

## 18. Docker и инфраструктура

### Сервисы (docker-compose.yml)

Файл: `infra/docker/docker-compose.yml`

| Сервис | Образ | Порт | Описание |
|--------|-------|------|---------|
| `postgres` | `postgres:17-alpine` | 5432 | Основная БД |
| `redis` | `redis:8-alpine` | 6379 | Кэш + очереди BullMQ |
| `coturn` | `coturn/coturn:latest` | 3478, 5349 | STUN/TURN для SIP |

### Запуск

```bash
# Dev-окружение
cd infra/docker
docker compose up -d

# Проверка статуса
docker compose ps

# Логи
docker compose logs -f postgres
docker compose logs -f redis

# Остановка
docker compose down

# Остановка с удалением данных
docker compose down -v
```

### Порты

| Приложение | Порт |
|-----------|------|
| NestJS API | 3000 |
| Web Admin (рецепция) | 3001 |
| Web Club Admin | 3002 |
| Web Platform Admin | 3003 |
| PostgreSQL | 5432 |
| Redis | 6379 |
| coturn STUN/TURN (UDP+TCP) | 3478 |
| coturn STUN/TURN TLS | 5349 |
| coturn RTP media range | 49152-65535/udp |

### Docker volumes

- `postgres_data` -- данные PostgreSQL
- `redis_data` -- данные Redis (AOF persistence)

---

## 19. Swagger / OpenAPI

### Доступ

Swagger UI доступен в dev-режиме по адресу:

```
http://localhost:3000/api/docs
```

### Конфигурация

Swagger настроен в `apps/api/src/main.ts`:

```typescript
const swaggerConfig = new DocumentBuilder()
  .setTitle('FitCRM API')
  .setDescription('CRM system for fitness clubs')
  .setVersion('1.0')
  .addBearerAuth()
  .addApiKey({ type: 'apiKey', name: 'X-API-Key', in: 'header' }, 'api-key')
  .build();

const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api/docs', app, document);
```

### Группы (Tags)

| Tag | Контроллер |
|-----|-----------|
| Auth | AuthController |
| Users | UsersController |
| Clients | ClientsController |
| Trainings | ScheduleController |
| Funnel | FunnelController |
| Sales | SalesController |
| Stats | StatsController |
| Notifications | NotificationsController |
| SIP | SipController |
| work-schedule | WorkScheduleController |
| Modules | ModulesController |
| Metering | MeteringController |
| Provisioning | ProvisioningController |

### Авторизация в Swagger UI

1. Открыть `http://localhost:3000/api/docs`
2. Нажать кнопку "Authorize"
3. Ввести JWT-токен в формате: `Bearer <token>` (для BearerAuth) или API-ключ (для api-key)
4. Все последующие запросы будут отправляться с авторизацией

### Генерация API-клиента

OpenAPI-спецификация может использоваться для автогенерации TypeScript-клиента:

```bash
pnpm --filter api openapi:generate
```

Сгенерированный клиент сохраняется в `packages/api-client/`.

---

## 20. Очереди задач (BullMQ)

FitCRM использует BullMQ поверх Redis для асинхронной обработки задач.

### Конфигурация

Модуль `QueueModule` (`apps/api/src/queue/queue.module.ts`) — глобальный `@Global()` модуль, подключающий BullMQ к Redis через `ConfigService`.

### Зарегистрированные очереди

| Очередь | Назначение | Retry | Backoff |
|---------|-----------|-------|---------|
| `webhook-delivery` | Доставка webhook-событий с HMAC-SHA256 подписью | 3 попытки | 5с → 30с → 5мин (экспоненциальный) |
| `push-notification` | Отправка push через FCM/APNs | 3 попытки | Экспоненциальный |
| `sync-1c` | Синхронизация данных с 1С:Фитнес | 3 попытки | Экспоненциальный |

### Архитектура событий

Бизнес-сервисы эмитят события через `EventEmitter2`:

```typescript
// clients.service.ts
this.eventEmitter.emit('client.created', { clubId, client });

// schedule.service.ts
this.eventEmitter.emit('training.created', { clubId, training });
```

Слушатели (`@OnEvent`) обрабатывают события и ставят задачи в очереди:

```
Бизнес-сервис → EventEmitter2 → WebhookDispatchService → BullMQ (webhook-delivery)
                               → NotificationTriggersService → BullMQ (push-notification)
```

### Процессор webhook-delivery

`WebhookDeliveryProcessor` (`apps/api/src/modules/webhooks/webhook-delivery.processor.ts`):

1. Получает job с данными события
2. Находит активные webhook-подписки клуба
3. Формирует payload с `event`, `timestamp`, `club_id`, `data`
4. Подписывает HMAC-SHA256 (`X-Webhook-Signature`)
5. Отправляет HTTP POST
6. Логирует результат в `webhook_logs`
7. При ошибке — автоматический retry

### 12 типов событий

`client.created`, `client.updated`, `training.created`, `training.cancelled`, `training.rescheduled`, `training.completed`, `sale.created`, `sale.paid`, `funnel.stage_changed`, `funnel.assigned`, `schedule.updated`, `membership.expiring`

---

## 21. Rate Limiting

API защищено rate limiting через `@nestjs/throttler` с Redis-хранилищем.

### Конфигурация

```typescript
// app.module.ts
ThrottlerModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    throttlers: [{ ttl: 60000, limit: 1000 }],
    storage: new ThrottlerStorageRedisService(config.get('REDIS_URL')),
  }),
})
```

### Параметры

| Параметр | Значение | Описание |
|----------|----------|---------|
| TTL | 60 000 мс (1 мин) | Окно времени |
| Limit | 1000 | Максимум запросов за окно |
| Storage | Redis | Централизованное хранение счётчиков |

### Поведение

- Guard `ThrottlerGuard` зарегистрирован глобально (`APP_GUARD`)
- При превышении лимита возвращается `429 Too Many Requests`
- Счётчики per-IP (или per-API-key для серверных интеграций)
- Redis-хранилище обеспечивает корректную работу с несколькими инстансами API

### Ответ при превышении

```json
{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests"
}
```

Заголовки ответа:

| Заголовок | Описание |
|-----------|---------|
| `X-RateLimit-Limit` | Максимум запросов за окно |
| `X-RateLimit-Remaining` | Оставшееся количество запросов |
| `X-RateLimit-Reset` | Время сброса окна (unix timestamp) |
