# 🗄️ СХЕМА БАЗЫ ДАННЫХ SFERA v2.0 - СИСТЕМА ПОСТАВОК > **⚠️ ВАЖНО:** Этот документ описывает НОВЫЕ таблицы для системы поставок v2.0. Существующие таблицы остаются без изменений для обратной совместимости. ## 📦 НОВЫЕ ТАБЛИЦЫ ПОСТАВОК НА ФУЛФИЛМЕНТ ### 1️⃣ **FulfillmentConsumableSupplyOrder - Поставки расходников ФФ** ```prisma model FulfillmentConsumableSupplyOrder { // === БАЗОВЫЕ ПОЛЯ === id String @id @default(cuid()) status SupplyOrderStatusV2 @default(PENDING) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === ДАННЫЕ ФФ (создатель) === fulfillmentCenterId String // кто заказывает (FK: Organization) requestedDeliveryDate DateTime // когда нужно resalePricePerUnit Decimal? @db.Decimal(10, 2) // цена продажи селлерам minStockLevel Int? // минимальный остаток notes String? // заметки ФФ // === ДАННЫЕ ПОСТАВЩИКА === supplierId String? // кто поставляет (FK: Organization) supplierApprovedAt DateTime? // когда одобрил packagesCount Int? // количество грузомест estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³ supplierContractId String? // номер договора supplierNotes String? // заметки поставщика // === ДАННЫЕ ЛОГИСТИКИ === logisticsPartnerId String? // кто везет (FK: Organization) estimatedDeliveryDate DateTime? // план доставки routeId String? // маршрут (FK: LogisticsRoute) logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки logisticsNotes String? // заметки логистики // === ДАННЫЕ ОТГРУЗКИ === shippedAt DateTime? // факт отгрузки trackingNumber String? // номер отслеживания // === ДАННЫЕ ПРИЕМКИ === receivedAt DateTime? // факт приемки receivedById String? // кто принял (FK: User) actualQuantity Int? // принято количество defectQuantity Int? // брак receiptNotes String? // заметки приемки // === СВЯЗИ === fulfillmentCenter Organization @relation("FFSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id]) supplier Organization? @relation("FFSupplyOrdersSupplier", fields: [supplierId], references: [id]) logisticsPartner Organization? @relation("FFSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id]) receivedBy User? @relation("FFSupplyOrdersReceiver", fields: [receivedById], references: [id]) items FulfillmentConsumableSupplyItem[] @@map("fulfillment_consumable_supply_orders") } model FulfillmentConsumableSupplyItem { id String @id @default(cuid()) supplyOrderId String // связь с поставкой productId String // какой расходник (FK: Product) // === КОЛИЧЕСТВА === requestedQuantity Int // запросили approvedQuantity Int? // поставщик одобрил shippedQuantity Int? // отгрузили receivedQuantity Int? // приняли defectQuantity Int? @default(0) // брак // === ЦЕНЫ === unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика totalPrice Decimal @db.Decimal(12, 2) // общая стоимость createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === СВЯЗИ === supplyOrder FulfillmentConsumableSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade) product Product @relation("FFSupplyItems", fields: [productId], references: [id]) @@unique([supplyOrderId, productId]) @@map("fulfillment_consumable_supply_items") } ``` ### 2️⃣ **SellerConsumableSupplyOrder - Поставки расходников селлеров** ```prisma model SellerConsumableSupplyOrder { // === БАЗОВЫЕ ПОЛЯ === id String @id @default(cuid()) status SupplyOrderStatusV2 @default(PENDING) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === ДАННЫЕ СЕЛЛЕРА (создатель) === sellerId String // кто заказывает (FK: Organization) fulfillmentCenterId String // где будет храниться (FK: Organization) requestedDeliveryDate DateTime // когда нужно notes String? // заметки селлера // === ДАННЫЕ ПОСТАВЩИКА === supplierId String? // кто поставляет (FK: Organization) supplierApprovedAt DateTime? // когда одобрил packagesCount Int? // количество грузомест estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³ supplierContractId String? // номер договора supplierNotes String? // заметки поставщика // === ДАННЫЕ ЛОГИСТИКИ === logisticsPartnerId String? // кто везет (FK: Organization) estimatedDeliveryDate DateTime? // план доставки routeId String? // маршрут (FK: LogisticsRoute) logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки logisticsNotes String? // заметки логистики // === ДАННЫЕ ОТГРУЗКИ === shippedAt DateTime? // факт отгрузки trackingNumber String? // номер отслеживания // === ДАННЫЕ ПРИЕМКИ === receivedAt DateTime? // факт приемки ФФ receivedById String? // кто принял (FK: User) actualQuantity Int? // принято количество defectQuantity Int? // брак receiptNotes String? // заметки приемки // === УНИКАЛЬНЫЕ ПОЛЯ ДЛЯ РАСХОДНИКОВ СЕЛЛЕРОВ === storageTermMonths Int? // срок хранения в месяцах accessRights SellerConsumableAccessRights @default(SELLER_ONLY) storageCostPerMonth Decimal? @db.Decimal(8, 2) // стоимость хранения за месяц // === СВЯЗИ === seller Organization @relation("SellerSupplyOrdersSeller", fields: [sellerId], references: [id]) fulfillmentCenter Organization @relation("SellerSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id]) supplier Organization? @relation("SellerSupplyOrdersSupplier", fields: [supplierId], references: [id]) logisticsPartner Organization? @relation("SellerSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id]) receivedBy User? @relation("SellerSupplyOrdersReceiver", fields: [receivedById], references: [id]) items SellerConsumableSupplyItem[] @@map("seller_consumable_supply_orders") } model SellerConsumableSupplyItem { id String @id @default(cuid()) supplyOrderId String // связь с поставкой productId String // какой расходник селлера (FK: Product) // === КОЛИЧЕСТВА === requestedQuantity Int // запросили approvedQuantity Int? // поставщик одобрил shippedQuantity Int? // отгрузили receivedQuantity Int? // приняли defectQuantity Int? @default(0) // брак // === ЦЕНЫ (видит только селлер) === unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика totalPrice Decimal @db.Decimal(12, 2) // общая стоимость createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === СВЯЗИ === supplyOrder SellerConsumableSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade) product Product @relation("SellerSupplyItems", fields: [productId], references: [id]) @@unique([supplyOrderId, productId]) @@map("seller_consumable_supply_items") } ``` ### 3️⃣ **GoodsSupplyOrder - Товарные поставки** ```prisma model GoodsSupplyOrder { // === БАЗОВЫЕ ПОЛЯ === id String @id @default(cuid()) status SupplyOrderStatusV2 @default(PENDING) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === ДАННЫЕ СЕЛЛЕРА (создатель) === sellerId String // кто заказывает (FK: Organization) fulfillmentCenterId String // куда доставить (FK: Organization) requestedDeliveryDate DateTime // когда нужно notes String? // заметки селлера // === ДАННЫЕ ПОСТАВЩИКА === supplierId String? // кто поставляет (FK: Organization) supplierApprovedAt DateTime? // когда одобрил packagesCount Int? // количество грузомест estimatedVolume Decimal? @db.Decimal(8, 3) // объем груза в м³ supplierContractId String? // номер договора supplierNotes String? // заметки поставщика // === ДАННЫЕ ЛОГИСТИКИ === logisticsPartnerId String? // кто везет (FK: Organization) estimatedDeliveryDate DateTime? // план доставки routeId String? // маршрут (FK: LogisticsRoute) logisticsCost Decimal? @db.Decimal(10, 2) // стоимость доставки logisticsNotes String? // заметки логистики // === ДАННЫЕ ОТГРУЗКИ === shippedAt DateTime? // факт отгрузки trackingNumber String? // номер отслеживания // === ДАННЫЕ ПРИЕМКИ === receivedAt DateTime? // факт приемки receivedById String? // кто принял (FK: User) actualQuantity Int? // принято количество defectQuantity Int? // брак receiptNotes String? // заметки приемки // === УНИКАЛЬНЫЕ ПОЛЯ ДЛЯ ТОВАРОВ === hasRecipes Boolean @default(false) // есть ли рецептуры totalServicesValue Decimal? @db.Decimal(12, 2) // общая стоимость услуг ФФ // === СВЯЗИ === seller Organization @relation("GoodsSupplyOrdersSeller", fields: [sellerId], references: [id]) fulfillmentCenter Organization @relation("GoodsSupplyOrdersFulfillment", fields: [fulfillmentCenterId], references: [id]) supplier Organization? @relation("GoodsSupplyOrdersSupplier", fields: [supplierId], references: [id]) logisticsPartner Organization? @relation("GoodsSupplyOrdersLogistics", fields: [logisticsPartnerId], references: [id]) receivedBy User? @relation("GoodsSupplyOrdersReceiver", fields: [receivedById], references: [id]) items GoodsSupplyItem[] @@map("goods_supply_orders") } model GoodsSupplyItem { id String @id @default(cuid()) supplyOrderId String // связь с поставкой productId String // какой товар (FK: Product) // === КОЛИЧЕСТВА === requestedQuantity Int // запросили approvedQuantity Int? // поставщик одобрил shippedQuantity Int? // отгрузили receivedQuantity Int? // приняли defectQuantity Int? @default(0) // брак // === ЦЕНЫ (видит только селлер) === unitPrice Decimal @db.Decimal(10, 2) // цена за единицу от поставщика totalPrice Decimal @db.Decimal(12, 2) // общая стоимость // === РЕЦЕПТУРА (JSON) === recipe Json? // полная рецептура в JSON /* recipe structure: { services: string[] // ID услуг ФФ fulfillmentConsumables: string[] // ID расходников ФФ sellerConsumables: string[] // ID расходников селлера marketplaceCardId?: string // карточка товара } */ createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === СВЯЗИ === supplyOrder GoodsSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade) product Product @relation("GoodsSupplyItems", fields: [productId], references: [id]) @@unique([supplyOrderId, productId]) @@map("goods_supply_items") } ``` --- ## 🛒 ТАБЛИЦЫ ПОСТАВОК НА МАРКЕТПЛЕЙСЫ ### 4️⃣ **OzonSupplyOrder - Поставки на Ozon** ```prisma model OzonSupplyOrder { // === БАЗОВЫЕ ПОЛЯ === id String @id @default(cuid()) status MarketplaceSupplyStatus @default(PLANNED) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === ДАННЫЕ ФФ (создатель) === fulfillmentCenterId String // кто отгружает (FK: Organization) plannedShipmentDate DateTime // план отгрузки notes String? // заметки ФФ // === СПЕЦИФИЧНЫЕ ПОЛЯ OZON === ozonWarehouseId String // склад Ozon ozonSupplyId String? // ID поставки в системе Ozon ozonPostingNumber String? // номер отправления Ozon // === ДАННЫЕ ОТГРУЗКИ === preparedAt DateTime? // готово к отгрузке shippedAt DateTime? // отгружено trackingNumber String? // номер отслеживания // === ДАННЫЕ ПРИЕМКИ МАРКЕТПЛЕЙСОМ === acceptedAt DateTime? // принято Ozon acceptedQuantity Int? // принято количество rejectedQuantity Int? // отклонено rejectionReason String? // причина отклонения // === СВЯЗИ === fulfillmentCenter Organization @relation("OzonSupplyOrders", fields: [fulfillmentCenterId], references: [id]) items OzonSupplyItem[] @@map("ozon_supply_orders") } model OzonSupplyItem { id String @id @default(cuid()) supplyOrderId String // связь с поставкой productId String // какой готовый продукт (FK: Product) // === КОЛИЧЕСТВА === plannedQuantity Int // планируется отгрузить preparedQuantity Int? // подготовлено shippedQuantity Int? // отгружено acceptedQuantity Int? // принято Ozon rejectedQuantity Int? @default(0) // отклонено // === OZON СПЕЦИФИЧНЫЕ ПОЛЯ === ozonProductId String? // ID товара в Ozon ozonSku String? // SKU в Ozon ozonBarcode String? // штрихкод createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === СВЯЗИ === supplyOrder OzonSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade) product Product @relation("OzonSupplyItems", fields: [productId], references: [id]) @@unique([supplyOrderId, productId]) @@map("ozon_supply_items") } ``` ### 5️⃣ **WildberriesSupplyOrder - Поставки на Wildberries** ```prisma model WildberriesSupplyOrder { // === БАЗОВЫЕ ПОЛЯ === id String @id @default(cuid()) status MarketplaceSupplyStatus @default(PLANNED) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === ДАННЫЕ ФФ (создатель) === fulfillmentCenterId String // кто отгружает (FK: Organization) plannedShipmentDate DateTime // план отгрузки notes String? // заметки ФФ // === СПЕЦИФИЧНЫЕ ПОЛЯ WILDBERRIES === wbWarehouseId String // склад WB wbSupplyId String? // ID поставки в системе WB wbStickerId String? // ID стикера WB // === ДАННЫЕ ОТГРУЗКИ === preparedAt DateTime? // готово к отгрузке shippedAt DateTime? // отгружено trackingNumber String? // номер отслеживания // === ДАННЫЕ ПРИЕМКИ МАРКЕТПЛЕЙСОМ === acceptedAt DateTime? // принято WB acceptedQuantity Int? // принято количество rejectedQuantity Int? // отклонено rejectionReason String? // причина отклонения // === СВЯЗИ === fulfillmentCenter Organization @relation("WildberriesSupplyOrders", fields: [fulfillmentCenterId], references: [id]) items WildberriesSupplyItem[] @@map("wildberries_supply_orders") } model WildberriesSupplyItem { id String @id @default(cuid()) supplyOrderId String // связь с поставкой productId String // какой готовый продукт (FK: Product) // === КОЛИЧЕСТВА === plannedQuantity Int // планируется отгрузить preparedQuantity Int? // подготовлено shippedQuantity Int? // отгружено acceptedQuantity Int? // принято WB rejectedQuantity Int? @default(0) // отклонено // === WB СПЕЦИФИЧНЫЕ ПОЛЯ === wbNmId String? // Номенклатура WB wbSku String? // SKU в WB wbBarcode String? // штрихкод wbSize String? // размер для WB createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // === СВЯЗИ === supplyOrder WildberriesSupplyOrder @relation(fields: [supplyOrderId], references: [id], onDelete: Cascade) product Product @relation("WBSupplyItems", fields: [productId], references: [id]) @@unique([supplyOrderId, productId]) @@map("wildberries_supply_items") } ``` --- ## 📊 НОВЫЕ ENUMS ### **SupplyOrderStatusV2 - Статусы поставок НА фулфилмент** ```prisma enum SupplyOrderStatusV2 { PENDING // Ожидает одобрения поставщика SUPPLIER_APPROVED // Одобрено поставщиком LOGISTICS_CONFIRMED // Логистика подтверждена SHIPPED // Отгружено поставщиком IN_TRANSIT // В пути DELIVERED // Доставлено и принято CANCELLED // Отменено } ``` ### **MarketplaceSupplyStatus - Статусы поставок НА маркетплейсы** ```prisma enum MarketplaceSupplyStatus { PLANNED // Запланирована PREPARED // Подготовлена к отгрузке SHIPPED_TO_MARKETPLACE // Отгружена на маркетплейс ACCEPTED_BY_MARKETPLACE // Принята маркетплейсом CANCELLED // Отменена } ``` ### **SellerConsumableAccessRights - Права доступа к расходникам селлеров** ```prisma enum SellerConsumableAccessRights { SELLER_ONLY // Только селлер может использовать SHARED_WITH_FF // ФФ тоже может использовать (с разрешения) } ``` --- ## 🔄 ОБНОВЛЕНИЯ СУЩЕСТВУЮЩИХ ТАБЛИЦ ### **Product - Добавление новых связей** ```prisma model Product { // ... существующие поля остаются без изменений // === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 === fulfillmentSupplyItems FulfillmentConsumableSupplyItem[] @relation("FFSupplyItems") sellerSupplyItems SellerConsumableSupplyItem[] @relation("SellerSupplyItems") goodsSupplyItems GoodsSupplyItem[] @relation("GoodsSupplyItems") ozonSupplyItems OzonSupplyItem[] @relation("OzonSupplyItems") wildberriesSupplyItems WildberriesSupplyItem[] @relation("WBSupplyItems") // ... остальные поля остаются без изменений } ``` ### **Organization - Добавление новых связей** ```prisma model Organization { // ... существующие поля остаются без изменений // === НОВЫЕ СВЯЗИ С ПОСТАВКАМИ V2 === // Поставки расходников ФФ fulfillmentSupplyOrdersAsFulfillment FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersFulfillment") fulfillmentSupplyOrdersAsSupplier FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersSupplier") fulfillmentSupplyOrdersAsLogistics FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersLogistics") // Поставки расходников селлеров sellerSupplyOrdersAsSeller SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersSeller") sellerSupplyOrdersAsFulfillment SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersFulfillment") sellerSupplyOrdersAsSupplier SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersSupplier") sellerSupplyOrdersAsLogistics SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersLogistics") // Товарные поставки goodsSupplyOrdersAsSeller GoodsSupplyOrder[] @relation("GoodsSupplyOrdersSeller") goodsSupplyOrdersAsFulfillment GoodsSupplyOrder[] @relation("GoodsSupplyOrdersFulfillment") goodsSupplyOrdersAsSupplier GoodsSupplyOrder[] @relation("GoodsSupplyOrdersSupplier") goodsSupplyOrdersAsLogistics GoodsSupplyOrder[] @relation("GoodsSupplyOrdersLogistics") // Поставки на маркетплейсы ozonSupplyOrders OzonSupplyOrder[] @relation("OzonSupplyOrders") wildberriesSupplyOrders WildberriesSupplyOrder[] @relation("WildberriesSupplyOrders") // ... остальные поля остаются без изменений } ``` ### **User - Добавление новых связей** ```prisma model User { // ... существующие поля остаются без изменений // === НОВЫЕ СВЯЗИ С ПРИЕМКОЙ ПОСТАВОК V2 === fulfillmentSupplyOrdersReceived FulfillmentConsumableSupplyOrder[] @relation("FFSupplyOrdersReceiver") sellerSupplyOrdersReceived SellerConsumableSupplyOrder[] @relation("SellerSupplyOrdersReceiver") goodsSupplyOrdersReceived GoodsSupplyOrder[] @relation("GoodsSupplyOrdersReceiver") // ... остальные поля остаются без изменений } ``` --- ## 🔍 ИНДЕКСЫ ДЛЯ ПРОИЗВОДИТЕЛЬНОСТИ ```prisma // Индексы для быстрого поиска поставок по статусу и организации @@index([fulfillmentCenterId, status]) @@index([sellerId, status]) @@index([supplierId, status]) @@index([logisticsPartnerId, status]) // Индексы для поиска по датам @@index([createdAt]) @@index([requestedDeliveryDate]) @@index([estimatedDeliveryDate]) // Индексы для отслеживания @@index([trackingNumber]) @@index([supplierContractId]) // Маркетплейс-специфичные индексы @@index([ozonSupplyId]) @@index([ozonProductId]) @@index([wbSupplyId]) @@index([wbNmId]) ``` --- ## 📋 ПЛАН МИГРАЦИИ ### **Phase 1: Создание новых таблиц** ```sql -- Создание таблиц параллельно с существующими CREATE TABLE fulfillment_consumable_supply_orders (...); CREATE TABLE fulfillment_consumable_supply_items (...); -- и т.д. ``` ### **Phase 2: Новые enum значения** ```sql -- Добавление новых enum без затрагивания старых CREATE TYPE "SupplyOrderStatusV2" AS ENUM (...); CREATE TYPE "MarketplaceSupplyStatus" AS ENUM (...); ``` ### **Phase 3: Обновление связей (без удаления старых)** ```sql -- Добавление новых foreign key связей ALTER TABLE "Product" ADD COLUMN ...; ALTER TABLE "Organization" ADD COLUMN ...; ``` ### **Phase 4: Данные миграции (только с одобрения)** ```sql -- Миграция данных из старых таблиц в новые -- ТОЛЬКО после полного тестирования новой системы ``` --- ## ⚠️ ВАЖНЫЕ ПРИНЦИПЫ ### ✅ **ОБРАТНАЯ СОВМЕСТИМОСТЬ** - Все существующие таблицы остаются без изменений - Новые таблицы создаются параллельно - Старая система продолжает работать ### 🔒 **БЕЗОПАСНОСТЬ ДАННЫХ** - Каждый тип поставки изолирован в отдельной таблице - Связи защищены foreign key constraints - Каскадное удаление только для зависимых записей ### 📈 **МАСШТАБИРУЕМОСТЬ** - Легко добавлять новые типы поставок - Оптимизированные индексы для каждого use case - Независимые схемы для разных процессов ### 🛡️ **ЦЕЛОСТНОСТЬ ДАННЫХ** - Строгие ограничения на связи между таблицами - Валидация через enum значения - Уникальные индексы предотвращают дублирование **Следующий шаг:** Создание Prisma миграций для FulfillmentConsumableSupplyOrder