# ПРАВИЛА PRISMA МОДЕЛЕЙ СИСТЕМЫ SFERA

## 🎯 ОБЩИЕ ПРИНЦИПЫ МОДЕЛИРОВАНИЯ

### 1. СОГЛАШЕНИЯ ИМЕНОВАНИЯ

```prisma
// ✅ Правильное именование
model Organization {
  id        String   @id @default(cuid())     // PascalCase для моделей
  createdAt DateTime @default(now())         // camelCase для полей
  updatedAt DateTime @updatedAt              // Автоматические временные метки
}

// ✅ Маппинг таблиц
@@map("organizations")  // snake_case для таблиц БД

// ❌ Неправильное именование
model organization { ... }          // Должно быть PascalCase
model User {
  user_id String  // Должно быть camelCase: userId
}
```

### 2. ОБЯЗАТЕЛЬНЫЕ ПОЛЯ ДЛЯ ВСЕХ МОДЕЛЕЙ

```prisma
model BaseModel {
  id        String   @id @default(cuid())  // Всегда CUID как PK
  createdAt DateTime @default(now())       // Дата создания
  updatedAt DateTime @updatedAt            // Автоматическое обновление

  @@map("base_models")
}
```

### 3. ТИПЫ ДАННЫХ И ОГРАНИЧЕНИЯ

```prisma
// ✅ Правильные типы для денежных величин
price      Decimal @db.Decimal(12, 2)  // Высокая точность
totalPrice Decimal @db.Decimal(15, 2)  // Для больших сумм

// ✅ JSON для гибких данных
phones       Json?    // Массивы телефонов
validationData Json?  // API данные

// ✅ Ограничения уникальности
inn          String @unique            // Уникальные бизнес-идентификаторы
phone        String @unique            // Уникальные контактные данные
referralCode String? @unique           // Опциональные уникальные коды
```

## 📋 ОСНОВНЫЕ ENUMS

### OrganizationType - Типы организаций

```prisma
enum OrganizationType {
  FULFILLMENT  // Фулфилмент-центры
  SELLER       // Селлеры (продавцы)
  LOGIST       // Логистические компании
  WHOLESALE    // Поставщики (оптовики)
}
```

**Правила использования:**

- ✅ Обязательное поле в Organization
- ✅ Определяет доступные связи и функции
- ❌ Нельзя изменить после создания организации

### SupplyType - Типы расходников

```prisma
enum SupplyType {
  FULFILLMENT_CONSUMABLES  // Расходники для операций ФФ
  SELLER_CONSUMABLES      // Расходники селлеров на хранении
}
```

**Критическое разделение:**

- `FULFILLMENT_CONSUMABLES`: Принадлежат ФФ, для внутренних операций
- `SELLER_CONSUMABLES`: Принадлежат селлеру, хранятся на складе ФФ

### SupplyOrderStatus - Статусы поставок

```prisma
enum SupplyOrderStatus {
  PENDING             // Ожидает одобрения поставщика
  SUPPLIER_APPROVED   // Одобрено поставщиком → логистика
  LOGISTICS_CONFIRMED // Подтверждено логистикой → отгрузка
  SHIPPED             // Отправлено → доставка
  DELIVERED           // Доставлено → завершено
  CANCELLED           // Отменено

  // Legacy (обратная совместимость):
  CONFIRMED           // → SUPPLIER_APPROVED
  IN_TRANSIT          // → SHIPPED
}
```

## 🏢 КОРНЕВЫЕ МОДЕЛИ СИСТЕМЫ

### User - Пользователи

```prisma
model User {
  id             String        @id @default(cuid())
  phone          String        @unique              // Уникальный идентификатор
  avatar         String?                           // Аватар (опционально)
  managerName    String?                           // Имя менеджера
  organizationId String?                           // Связь с организацией
  createdAt      DateTime      @default(now())
  updatedAt      DateTime      @updatedAt

  // Связи
  organization   Organization? @relation(fields: [organizationId], references: [id])
  sentMessages   Message[]     @relation("SentMessages")
  smsCodes       SmsCode[]

  @@map("users")
}
```

**Критические правила:**

- ✅ `phone` - единственный способ входа в систему
- ✅ Один пользователь может быть связан только с одной организацией
- ✅ `organizationId` опциональный - пользователь может существовать без организации

### Organization - Организации

```prisma
model Organization {
  id                String              @id @default(cuid())
  inn               String              @unique        // Уникальный ИНН
  type              OrganizationType                   // Тип организации
  name              String?                            // Название (опционально)
  fullName          String?                            // Полное название

  // Реквизиты из API Dadata
  ogrn              String?
  address           String?
  // ... другие поля из API
  dadataData        Json?                              // Полные данные Dadata

  // Реферальная система
  referralCode      String?             @unique        // Уникальный реферальный код
  referredById      String?                            // Кто пригласил
  referralPoints    Int                 @default(0)    // Накопленные баллы

  // Связи (критически важные)
  users             User[]                             // Пользователи организации
  apiKeys           ApiKey[]                           // Ключи маркетплейсов

  // Партнерство
  counterpartyOf             Counterparty[] @relation("CounterpartyOf")
  organizationCounterparties Counterparty[] @relation("OrganizationCounterparties")
  receivedRequests           CounterpartyRequest[] @relation("ReceivedRequests")
  sentRequests               CounterpartyRequest[] @relation("SentRequests")

  @@map("organizations")
}
```

**Критические правила:**

- ✅ `inn` уникален - одна организация = один ИНН
- ✅ `type` определяет доступный функционал
- ✅ `referralCode` генерируется автоматически при создании
- ❌ Нельзя изменить `type` после создания

## 🔄 МОДЕЛИ БИЗНЕС-ПРОЦЕССОВ

### SupplyOrder - Заказы поставок

```prisma
model SupplyOrder {
  id                  String            @id @default(cuid())
  organizationId      String                              // Заказчик
  partnerId           String                              // Поставщик
  fulfillmentCenterId String?                             // ФФ-получатель
  logisticsPartnerId  String?                             // Логистика

  deliveryDate        DateTime                            // Желаемая дата доставки
  status              SupplyOrderStatus @default(PENDING) // Текущий статус

  // Суммарные данные
  totalAmount         Decimal           @db.Decimal(12, 2) // Общая сумма
  totalItems          Int                                  // Количество позиций

  // Параметры поставки (дополнительные)
  packagesCount       Int?                                // Количество грузовых мест - параметр поставки
  volume              Float?                              // Объём груза в м³ - параметр поставки

  // Управление
  responsibleEmployee String?                             // ID ответственного
  notes               String?                             // Комментарии
  consumableType      String?                             // Тип расходников

  createdAt           DateTime          @default(now())
  updatedAt           DateTime          @updatedAt

  // Связи
  organization        Organization @relation(fields: [organizationId], references: [id])
  partner             Organization @relation("SupplyOrderPartner", fields: [partnerId], references: [id])
  items               SupplyOrderItem[]
  routes              SupplyRoute[]

  @@map("supply_orders")
}
```

**Workflow правила:**

- ✅ `status` может изменяться только по определенной последовательности
- ✅ `partnerId` - всегда WHOLESALE организация
- ✅ `fulfillmentCenterId` - всегда FULFILLMENT организация
- ❌ Нельзя удалить SupplyOrder со статусом DELIVERED

### Supply - Расходники/Товары

```prisma
model Supply {
  id             String      @id @default(cuid())
  name           String                              // Название
  article        String                              // Артикул
  description    String?                             // Описание
  price          Decimal     @db.Decimal(10, 2)      // Цена за единицу
  quantity       Int         @default(0)             // Количество
  unit           String      @default("шт")          // Единица измерения

  // Классификация
  type           SupplyType  @default(FULFILLMENT_CONSUMABLES)
  category       String      @default("Расходники")

  // Управление складом
  minStock       Int         @default(0)             // Минимальный остаток
  currentStock   Int         @default(0)             // Текущий остаток
  usedStock      Int         @default(0)             // Использованное количество

  // Для SELLER_CONSUMABLES
  sellerOwnerId  String?                             // ID селлера-владельца

  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])

  @@map("supplies")
}
```

**Критические правила типов:**

- ✅ `FULFILLMENT_CONSUMABLES`: `sellerOwnerId` должен быть NULL
- ✅ `SELLER_CONSUMABLES`: `sellerOwnerId` обязателен
- ✅ `organizationId` для SELLER_CONSUMABLES = ID фулфилмента (место хранения)
- ❌ Нельзя изменить `type` после создания расходника

## 🤝 МОДЕЛИ ПАРТНЕРСТВА

### Counterparty - Контрагенты

```prisma
model Counterparty {
  id                String       @id @default(cuid())
  organizationId    String                           // Моя организация
  counterpartyId    String                           // Организация-контрагент
  type              CounterpartyType                 // Тип партнерства
  createdAt         DateTime     @default(now())

  // Связи
  organization      Organization @relation("OrganizationCounterparties", fields: [organizationId], references: [id])
  counterpartyOrg   Organization @relation("CounterpartyOf", fields: [counterpartyId], references: [id])

  @@unique([organizationId, counterpartyId])          // Уникальность пары
  @@map("counterparties")
}
```

### CounterpartyRequest - Заявки на партнерство

```prisma
model CounterpartyRequest {
  id           String                    @id @default(cuid())
  fromId       String                                 // Отправитель
  toId         String                                 // Получатель
  status       CounterpartyRequestStatus @default(PENDING)
  message      String?                                // Сообщение заявки
  createdAt    DateTime                  @default(now())
  updatedAt    DateTime                  @updatedAt

  // Связи
  from         Organization @relation("SentRequests", fields: [fromId], references: [id])
  to           Organization @relation("ReceivedRequests", fields: [toId], references: [id])

  @@unique([fromId, toId])                           // Одна заявка между организациями
  @@map("counterparty_requests")
}
```

**Правила партнерства:**

- ✅ Организация не может отправить заявку сама себе
- ✅ Между двумя организациями может быть только одна активная заявка
- ✅ При принятии заявки (ACCEPTED) создается Counterparty запись

## 📊 МОДЕЛИ ИНТЕГРАЦИЙ

### ApiKey - Ключи маркетплейсов

```prisma
model ApiKey {
  id             String          @id @default(cuid())
  marketplace    MarketplaceType              // WB/Ozon
  apiKey         String                       // Зашифрованный ключ
  isActive       Boolean         @default(true)
  validationData Json?                        // Данные валидации
  organizationId String

  organization   Organization @relation(fields: [organizationId], references: [id])

  @@unique([organizationId, marketplace])      // Один ключ на маркетплейс
  @@map("api_keys")
}
```

**Правила безопасности:**

- ✅ `apiKey` хранится в зашифрованном виде
- ✅ Только одни ключ на маркетплейс на организацию
- ✅ `validationData` содержит результаты проверки ключа
- ❌ Нельзя получить расшифрованный ключ через GraphQL

## 🎯 ПРАВИЛА СВЯЗЕЙ (RELATIONS)

### 1. КАСКАДНЫЕ УДАЛЕНИЯ

```prisma
// ✅ Правильное использование onDelete
model Organization {
  supplies Supply[] @relation(onDelete: Cascade)     // Удалить все расходники
  apiKeys  ApiKey[] @relation(onDelete: Cascade)     // Удалить все ключи
}

model SupplyOrder {
  items SupplyOrderItem[] @relation(onDelete: Cascade) // Удалить все позиции
}

// ❌ Неправильно - потеря критических данных
model User {
  organization Organization? @relation(onDelete: Cascade) // Не удалять организацию!
}
```

### 2. ОБЯЗАТЕЛЬНЫЕ И ОПЦИОНАЛЬНЫЕ СВЯЗИ

```prisma
// ✅ Обязательные связи
model Supply {
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])
}

// ✅ Опциональные связи
model User {
  organizationId String?
  organization   Organization? @relation(fields: [organizationId], references: [id])
}
```

### 3. ИМЕНОВАНИЕ СВЯЗЕЙ

```prisma
// ✅ Явные имена связей для множественных отношений
model Organization {
  // Отправленные заявки
  sentRequests     CounterpartyRequest[] @relation("SentRequests")
  // Полученные заявки
  receivedRequests CounterpartyRequest[] @relation("ReceivedRequests")

  // Я контрагент для кого-то
  counterpartyOf             Counterparty[] @relation("CounterpartyOf")
  // Мои контрагенты
  organizationCounterparties Counterparty[] @relation("OrganizationCounterparties")
}
```

## 🔒 ПРАВИЛА БЕЗОПАСНОСТИ

### 1. ИНДЕКСИРОВАНИЕ ДЛЯ ПРОИЗВОДИТЕЛЬНОСТИ

```prisma
model Organization {
  inn  String @unique            // Автоматический индекс
  type OrganizationType

  @@index([type])                // Индекс для поиска по типу
}

model SupplyOrder {
  organizationId String
  status         SupplyOrderStatus
  createdAt      DateTime

  @@index([organizationId, status])    // Составной индекс для фильтрации
  @@index([createdAt])                 // Индекс для сортировки по дате
}
```

### 2. ОГРАНИЧЕНИЯ УНИКАЛЬНОСТИ

```prisma
model Counterparty {
  organizationId   String
  counterpartyId   String

  @@unique([organizationId, counterpartyId]) // Предотвращает дубли
}

model ApiKey {
  organizationId String
  marketplace    MarketplaceType

  @@unique([organizationId, marketplace])    // Один ключ на маркетплейс
}
```

### 3. ВАЛИДАЦИЯ НА УРОВНЕ БД

```prisma
model Supply {
  quantity     Int         @default(0)        // Не может быть отрицательным
  currentStock Int         @default(0)
  price        Decimal     @db.Decimal(10, 2) // Точность денежных сумм

  // Проверки через CHECK constraints (на уровне БД):
  // CHECK (quantity >= 0)
  // CHECK (currentStock >= 0)
  // CHECK (price >= 0)
}
```

## 🔄 ПРАВИЛА МИГРАЦИЙ

### 1. БЕЗОПАСНЫЕ ИЗМЕНЕНИЯ (не ломают код)

```prisma
// ✅ Добавление новых опциональных полей
model Organization {
  // Существующие поля...
  newField String?  // Новое поле - nullable
}

// ✅ Добавление новых моделей
model NewFeature {
  id String @id @default(cuid())
  // ...поля
}
```

### 2. ОПАСНЫЕ ИЗМЕНЕНИЯ (ломают код)

```prisma
// ❌ Изменение типа существующего поля
model Organization {
  // Было: referralPoints Int
  referralPoints Float  // ЛОМАЕТ существующий код
}

// ❌ Удаление существующих полей
model User {
  // phone String @unique - УДАЛЕНО, ЛОМАЕТ код
  email String @unique  // Заменено на email
}

// ❌ Изменение обязательности поля
model Organization {
  // Было: name String?
  name String!  // ЛОМАЕТ записи с NULL
}
```

### 3. СТРАТЕГИЯ БЕЗОПАСНЫХ МИГРАЦИЙ

```prisma
// Этап 1: Добавить новое поле (nullable)
model Organization {
  oldField String?  // Старое поле
  newField String?  // Новое поле
}

// Этап 2: Заполнить данные в коде приложения
// UPDATE organizations SET newField = oldField WHERE newField IS NULL

// Этап 3: Сделать поле обязательным
model Organization {
  oldField String?  // Еще оставляем
  newField String!  // Теперь обязательное
}

// Этап 4: Удалить старое поле (через несколько версий)
model Organization {
  newField String!  // Только новое поле
}
```

---

_Извлечено из анализа: Prisma schema, бизнес-логика, правила БД_  
_Дата создания: 2025-08-21_  
_Основано на файле: prisma/schema.prisma_
