# ПРАВИЛА 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_