Files
sfera-new/docs/data-layer/PRISMA_MODEL_RULES.md
Veronika Smirnova 12fd8ddf61 feat(supplier-orders): добавить параметры поставки в таблицу заявок
- Добавлены колонки Объём и Грузовые места между Цена товаров и Статус
- Реализованы инпуты для ввода 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>
2025-08-23 18:47:23 +03:00

19 KiB
Raw Blame History

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

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

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

// ✅ Правильное именование
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. ОБЯЗАТЕЛЬНЫЕ ПОЛЯ ДЛЯ ВСЕХ МОДЕЛЕЙ

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

  @@map("base_models")
}

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

// ✅ Правильные типы для денежных величин
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 - Типы организаций

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

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

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

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

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

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

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

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

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

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

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

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

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 - Организации

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 - Заказы поставок

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 - Расходники/Товары

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 - Контрагенты

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 - Заявки на партнерство

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 - Ключи маркетплейсов

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. КАСКАДНЫЕ УДАЛЕНИЯ

// ✅ Правильное использование 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. ОБЯЗАТЕЛЬНЫЕ И ОПЦИОНАЛЬНЫЕ СВЯЗИ

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

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

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

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

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

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

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

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

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

model SupplyOrder {
  organizationId String
  status         SupplyOrderStatus
  createdAt      DateTime

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

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

model Counterparty {
  organizationId   String
  counterpartyId   String

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

model ApiKey {
  organizationId String
  marketplace    MarketplaceType

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

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

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. БЕЗОПАСНЫЕ ИЗМЕНЕНИЯ (не ломают код)

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

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

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

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

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

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

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

// Этап 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