
- Добавлены колонки Объём и Грузовые места между Цена товаров и Статус - Реализованы инпуты для ввода volume и packagesCount в статусе PENDING для роли WHOLESALE - Добавлена мутация UPDATE_SUPPLY_PARAMETERS с проверками безопасности - Скрыта строка Поставщик для роли WHOLESALE (поставщик знает свои данные) - Исправлено выравнивание таблицы при скрытии уровня поставщика - Реорганизованы документы: legacy-rules/, docs/, docs-and-reports/ ВНИМАНИЕ: Компонент multilevel-supplies-table.tsx (1697 строк) нарушает правило модульной архитектуры (>800 строк требует рефакторинга) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
514 lines
19 KiB
Markdown
514 lines
19 KiB
Markdown
# ПРАВИЛА 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_
|