Files
sfera-new/docs/development/INVENTORY_SYSTEM_ARCHITECTURE.md
Veronika Smirnova cdeee82237 feat: реализовать полную автосинхронизацию V2 системы расходников с nameForSeller и анализ миграции
-  Добавлено поле nameForSeller в FulfillmentConsumable для кастомизации названий
-  Добавлено поле inventoryId для связи между каталогом и складом
-  Реализована автосинхронизация FulfillmentConsumableInventory → FulfillmentConsumable
-  Обновлен UI с колонкой "Название для селлера" в /fulfillment/services/consumables
-  Исправлены GraphQL запросы (удалено поле description, добавлены новые поля)
-  Создан скрипт sync-inventory-to-catalog.ts для миграции существующих данных
-  Добавлена техническая документация архитектуры системы инвентаря
-  Создан отчет о статусе миграции V1→V2 с детальным планом

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 23:17:42 +03:00

41 KiB
Raw Blame History

📦 АРХИТЕКТУРА СИСТЕМЫ ИНВЕНТАРЯ SFERA

Версия: 2.0
Дата: 2025-09-03
Статус: Активная разработка (миграция V1→V2)


🎯 ОБЗОР СИСТЕМЫ

Система инвентаря SFERA представляет собой комплексную архитектуру для управления складскими остатками, ценообразованием и бизнес-процессами между 4 типами организаций: FULFILLMENT, SELLER, WHOLESALE, LOGIST.

🏗️ АРХИТЕКТУРНЫЕ ПРИНЦИПЫ

  1. Доменная изоляция - каждая организация видит только свои данные
  2. Двойная система учета - V1 (legacy) + V2 (современная) с автосинхронизацией
  3. Модульная структура - независимые резолверы и компоненты
  4. Типобезопасность - полная типизация TypeScript + GraphQL

📊 МОДЕЛИ ДАННЫХ

🔄 V2 СИСТЕМА (Актуальная)

FulfillmentConsumableInventory - Складские остатки фулфилмента

model FulfillmentConsumableInventory {
  id                  String       @id @default(cuid())
  fulfillmentCenterId String       // Изоляция по фулфилменту
  productId           String       // Связь с каталогом товаров
  
  // СКЛАДСКИЕ ДАННЫЕ
  currentStock        Int          @default(0)    // Текущий остаток
  minStock           Int          @default(0)     // Минимальный порог  
  maxStock           Int?                         // Максимальный порог
  reservedStock      Int          @default(0)     // Зарезервировано
  totalReceived      Int          @default(0)     // Всего получено
  totalShipped       Int          @default(0)     // Всего отгружено
  
  // ЦЕНЫ
  averageCost        Decimal      @default(0) @db.Decimal(10, 2) // Себестоимость
  resalePrice        Decimal?     @db.Decimal(10, 2)             // Цена селлерам
  
  // МЕТАДАННЫЕ
  lastSupplyDate     DateTime?    // Последняя поставка
  lastUsageDate      DateTime?    // Последнее использование
  notes              String?      // Заметки
  
  // СВЯЗИ
  fulfillmentCenter  Organization @relation(\"FFInventory\")
  product           Product      @relation(\"InventoryProducts\")
  catalogItems      FulfillmentConsumable[] // → Каталог услуг
  
  @@unique([fulfillmentCenterId, productId])
}

FulfillmentConsumable - Каталог услуг фулфилмента

model FulfillmentConsumable {
  id             String       @id @default(cuid())
  fulfillmentId  String       // Владелец услуги
  inventoryId    String?      // 🔗 Связь со складом
  
  // КАТАЛОЖНЫЕ ДАННЫЕ
  name           String       // Оригинальное название
  nameForSeller  String?      // 🆕 Кастомное название для селлеров
  article        String?      // Артикул
  pricePerUnit   Decimal      @default(0) @db.Decimal(10, 2) // Цена селлерам
  unit           String       @default(\"шт\")
  
  // СКЛАДСКИЕ ДАННЫЕ (синхронизируются из Inventory)
  currentStock   Int          @default(0)     // Текущий остаток
  minStock       Int          @default(0)     // Минимальный порог
  isAvailable    Boolean      @default(false) // Доступность
  
  // МЕТАДАННЫЕ
  imageUrl       String?      // Фото товара
  sortOrder      Int          @default(0)     // Порядок сортировки
  createdAt      DateTime     @default(now())
  updatedAt      DateTime     @updatedAt
  
  // СВЯЗИ
  fulfillment    Organization @relation(\"FulfillmentServices\")
  inventory      FulfillmentConsumableInventory? @relation(\"CatalogInventory\")
}

SellerGoodsInventory - Складские остатки товаров селлера

model SellerGoodsInventory {
  id                  String       @id @default(cuid())
  sellerId            String       // Владелец товаров
  fulfillmentCenterId String       // Где хранится
  productId           String       // Что хранится
  
  // СКЛАДСКИЕ ДАННЫЕ
  currentStock        Int          @default(0)     // Доступно
  reservedStock       Int          @default(0)     // Зарезервировано
  inPreparationStock  Int          @default(0)     // В подготовке
  totalReceived       Int          @default(0)     // Всего получено
  totalShipped        Int          @default(0)     // Всего отгружено
  
  // АВТОЗАКАЗЫ
  minStock           Int          @default(0)      // Порог автозаказа
  maxStock           Int?                          // Максимальный остаток
  
  // ЦЕНЫ
  averageCost        Decimal      @default(0) @db.Decimal(10, 2) // Себестоимость
  salePrice          Decimal      @default(0) @db.Decimal(10, 2) // Цена продажи
  
  // СВЯЗИ
  seller             Organization @relation(\"SellerInventory\")
  fulfillmentCenter  Organization @relation(\"SellerInventoryWarehouse\")
  product            Product      @relation(\"SellerInventoryProducts\")
  
  @@unique([sellerId, fulfillmentCenterId, productId])
}

🔄 БИЗНЕС-ПРОЦЕССЫ

📋 ПОТОК ТОВАРОВ: СЕЛЛЕР → ПОСТАВЩИК → ФУЛФИЛМЕНТ

sequenceDiagram
    participant S as Селлер
    participant UI as CreateSupplyForm
    participant API as GraphQL API
    participant W as Поставщик
    participant L as Логистика  
    participant F as Фулфилмент
    participant INV as InventorySystem

    S->>UI: 1. Создает поставку
    UI->>API: 2. createSellerGoodsSupply()
    API->>W: 3. Уведомление о заказе
    W->>API: 4. Одобряет заказ
    API->>L: 5. Назначение логистики
    L->>API: 6. Подтверждение доставки
    API->>F: 7. Уведомление о приемке
    F->>INV: 8. processSupplyOrderReceipt()
    INV->>INV: 9. updateInventory() - V2
    INV->>INV: 10. Автосинхронизация V1

🎯 ДЕТАЛЬНЫЕ ЭТАПЫ ПРОЦЕССА

ЭТАП 1: СОЗДАНИЕ ЗАКАЗА (Селлер)

UI Компоненты:

// CreateSuppliersSupplyPage/index.tsx
const CreateSuppliersSupplyPage = () => {
  const [selectedSupplier, setSelectedSupplier] = useState<Partner | null>(null)
  const [selectedGoods, setSelectedGoods] = useState<GoodsItem[]>([])
  const [productRecipes, setProductRecipes] = useState<ProductRecipes>({})
  const [deliverySettings, setDeliverySettings] = useState<DeliverySettings>({})

  const handleCreateSupply = async () => {
    // Валидация формы
    if (!isFormValid) return

    // Трансформация V1 → V2
    const v2InputData = adaptV1ToV2Format(formData, selectedGoods, productRecipes)

    // Создание поставки
    await createSellerGoodsSupply({
      variables: { input: v2InputData }
    })
  }
}

GraphQL Мутация:

mutation CreateSellerGoodsSupply($input: CreateSellerGoodsSupplyInput!) {
  createSellerGoodsSupply(input: $input) {
    success
    message
    supplyOrder {
      id
      status
      totalAmount
      seller { id name }
      supplier { id name }
      fulfillmentCenter { id name }
      recipeItems {
        id
        productId
        quantity
        recipeType
        product { name article price }
      }
    }
  }
}

ЭТАП 2: ОБРАБОТКА ЗАКАЗА (Backend)

Резолвер:

// goods-supply-v2.ts
createSellerGoodsSupply: async (_, { input }, context) => {
  // 1. Валидация пользователя
  const user = await validateSellerUser(context)
  
  // 2. Проверка партнерских отношений
  const fulfillmentPartner = await validatePartnership(
    user.organizationId, 
    input.fulfillmentCenterId, 
    'FULFILLMENT'
  )
  
  // 3. Создание поставки
  const supplyOrder = await prisma.sellerGoodsSupplyOrder.create({
    data: {
      sellerId: user.organizationId,
      fulfillmentCenterId: input.fulfillmentCenterId,
      supplierId: input.supplierId,
      status: 'PENDING',
      requestedDeliveryDate: new Date(input.requestedDeliveryDate),
      notes: input.notes,
      totalAmount: calculateTotalAmount(input.recipeItems),
      
      // 4. Нормализованная рецептура  
      recipeItems: {
        create: input.recipeItems.map(item => ({
          productId: item.productId,
          quantity: item.quantity,
          recipeType: item.recipeType,
          unitPrice: item.unitPrice,
          totalPrice: item.unitPrice * item.quantity,
        }))
      }
    },
    include: {
      recipeItems: { include: { product: true }},
      seller: true,
      supplier: true,
      fulfillmentCenter: true
    }
  })
  
  // 5. Уведомления участникам
  await notifyOrderCreated(supplyOrder)
  
  return {
    success: true,
    message: 'Поставка успешно создана',
    supplyOrder
  }
}

ЭТАП 3: ПРИЕМКА НА СКЛАДЕ (Фулфилмент)

Мутация приемки:

// fulfillment-goods-v2.ts  
receiveSellerGoodsSupply: async (_, { input }, context) => {
  // 1. Обновление статуса поставки
  const supplyOrder = await prisma.sellerGoodsSupplyOrder.update({
    where: { id: input.supplyOrderId },
    data: { 
      status: 'DELIVERED',
      deliveredAt: new Date(),
      actualPackagesCount: input.actualPackagesCount,
      actualVolume: input.actualVolume
    }
  })
  
  // 2. Обработка приемки через inventory-management
  await processSellerGoodsSupplyReceipt(
    input.supplyOrderId,
    input.receivedItems // [ { productId, receivedQuantity, unitPrice } ]
  )
}

Управление инвентарем:

// inventory-management-goods.ts
export async function processSellerGoodsSupplyReceipt(
  supplyOrderId: string,
  items: ReceivedItem[]
): Promise<void> {
  for (const item of items) {
    // Обновляем инвентарь товаров селлера
    await updateSellerGoodsInventory({
      sellerId: supplyOrder.sellerId,
      fulfillmentCenterId: supplyOrder.fulfillmentCenterId,
      productId: item.productId,
      quantity: item.receivedQuantity,
      type: 'INCOMING',
      sourceType: 'SUPPLY_ORDER',
      unitCost: item.unitPrice
    })
  }
}

💰 СИСТЕМА ЦЕНООБРАЗОВАНИЯ

🎯 ДВУХУРОВНЕВАЯ СИСТЕМА ЦЕН

1. СЕБЕСТОИМОСТЬ (averageCost)

// Рассчитывается автоматически методом взвешенной средней
async function recalculateAverageCost(
  inventoryId: string, 
  newQuantity: number, 
  newUnitCost: number
): Promise<void> {
  const inventory = await prisma.fulfillmentConsumableInventory.findUnique({
    where: { id: inventoryId }
  })

  const oldTotalValue = inventory.averageCost * (inventory.currentStock - newQuantity)
  const newTotalValue = newUnitCost * newQuantity
  const totalQuantity = inventory.currentStock
  
  const newAverageCost = (oldTotalValue + newTotalValue) / totalQuantity

  await prisma.fulfillmentConsumableInventory.update({
    where: { id: inventoryId },
    data: { averageCost: newAverageCost }
  })
}

2. ЦЕНА ДЛЯ СЕЛЛЕРОВ (resalePrice/pricePerUnit)

// Устанавливается фулфилментом вручную
updateFulfillmentConsumable: async (_, { input }) => {
  await prisma.fulfillmentConsumable.update({
    where: { id: input.id },
    data: {
      pricePerUnit: input.pricePerUnit,        // Цена для селлеров
      nameForSeller: input.nameForSeller       // Название для селлеров
    }
  })
  
  // 🔄 Синхронизация с системой инвентаря
  await prisma.fulfillmentConsumableInventory.update({
    where: { id: catalogItem.inventoryId },
    data: { resalePrice: input.pricePerUnit }
  })
}

3. МАРЖА И АНАЛИТИКА

interface PriceAnalytics {
  averageCost: number      // Себестоимость
  resalePrice: number      // Цена продажи
  margin: number           // Абсолютная маржа
  marginPercent: number    // Процент маржи
  turnoverDays: number     // Оборачиваемость в днях
}

const calculateMargin = (averageCost: number, resalePrice: number) => ({
  margin: resalePrice - averageCost,
  marginPercent: ((resalePrice - averageCost) / averageCost) * 100
})

// Пример: себестоимость 15.50₽, цена 25.00₽ = 61% маржа

🔄 АВТОСИНХРОНИЗАЦИЯ V1 ↔ V2

🎯 СТРАТЕГИЯ СИНХРОНИЗАЦИИ

// inventory-management.ts - основная логика синхронизации
export async function updateInventory(movement: InventoryMovement): Promise<void> {
  // 1. Обновление V2 системы (источник истины)
  const inventory = await prisma.fulfillmentConsumableInventory.upsert({
    where: {
      fulfillmentCenterId_productId: {
        fulfillmentCenterId: movement.fulfillmentCenterId,
        productId: movement.productId,
      },
    },
    create: {
      fulfillmentCenterId: movement.fulfillmentCenterId,
      productId: movement.productId,
      currentStock: movement.quantity,
      totalReceived: movement.type === 'INCOMING' ? movement.quantity : 0,
      totalShipped: movement.type === 'OUTGOING' ? movement.quantity : 0,
      averageCost: movement.unitCost || 0,
      lastSupplyDate: movement.type === 'INCOMING' ? new Date() : undefined,
      lastUsageDate: movement.type === 'OUTGOING' ? new Date() : undefined,
    },
    update: {
      currentStock: {
        increment: movement.type === 'INCOMING' ? movement.quantity : -movement.quantity,
      },
      totalReceived: movement.type === 'INCOMING' 
        ? { increment: movement.quantity } 
        : undefined,
      totalShipped: movement.type === 'OUTGOING' 
        ? { increment: movement.quantity } 
        : undefined,
      lastSupplyDate: movement.type === 'INCOMING' ? new Date() : undefined,
      lastUsageDate: movement.type === 'OUTGOING' ? new Date() : undefined,
    },
    include: { product: true },
  })

  // 2. 🔄 АВТОСИНХРОНИЗАЦИЯ V1 ← V2 (при поступлении товаров)
  if (movement.type === 'INCOMING') {
    try {
      const existingCatalogItem = await prisma.fulfillmentConsumable.findFirst({
        where: {
          fulfillmentId: movement.fulfillmentCenterId,
          name: inventory.product.name,
        },
      })

      if (existingCatalogItem) {
        // Обновляем существующую запись в каталоге
        await prisma.fulfillmentConsumable.update({
          where: { id: existingCatalogItem.id },
          data: {
            currentStock: inventory.currentStock,
            isAvailable: inventory.currentStock > 0,
            inventoryId: inventory.id,
            updatedAt: new Date(),
          },
        })
      } else {
        // Создаем новую запись в каталоге
        await prisma.fulfillmentConsumable.create({
          data: {
            fulfillmentId: movement.fulfillmentCenterId,
            inventoryId: inventory.id,
            name: inventory.product.name,
            article: inventory.product.article || '',
            pricePerUnit: 0, // Цену устанавливает фулфилмент вручную
            unit: inventory.product.unit || 'шт',
            minStock: inventory.minStock,
            currentStock: inventory.currentStock,
            isAvailable: inventory.currentStock > 0,
            imageUrl: inventory.product.imageUrl,
            sortOrder: 0,
          },
        })
      }
    } catch (syncError) {
      console.error('⚠️ Failed to sync FulfillmentConsumable:', syncError)
      // Не останавливаем процесс, если синхронизация не удалась
    }
  }

  // 3. Пересчет средневзвешенной стоимости
  if (movement.type === 'INCOMING' && movement.unitCost) {
    await recalculateAverageCost(inventory.id, movement.quantity, movement.unitCost)
  }
}

📊 ТРИГГЕРЫ СИНХРОНИЗАЦИИ

// Когда срабатывает автосинхронизация:

1. **Поступление товаров**  updateInventory(type: 'INCOMING')
   ├── Обновляет FulfillmentConsumableInventory
   └── Создает/обновляет FulfillmentConsumable

2. **Использование товаров**  updateInventory(type: 'OUTGOING')  
   ├── Обновляет FulfillmentConsumableInventory
   └── Обновляет FulfillmentConsumable.currentStock

3. **Изменение цен**  updateFulfillmentConsumable()
   ├── Обновляет FulfillmentConsumable.pricePerUnit
   └── Синхронизирует с FulfillmentConsumableInventory.resalePrice

4. **Одноразовая миграция**  syncInventoryToCatalog()
   └── Переносит все данные из Системы B в Систему A

🔧 ТЕХНИЧЕСКИЕ ДЕТАЛИ

📁 СТРУКТУРА ФАЙЛОВ

# INVENTORY CORE
/src/lib/inventory-management.ts              # Ядро системы управления остатками
/src/lib/inventory-management-goods.ts        # Управление товарным инвентарем
/src/scripts/sync-inventory-to-catalog.ts     # Скрипт одноразовой синхронизации

# GRAPHQL LAYER
/src/graphql/resolvers/
├── fulfillment-services-v2.ts               # V2 услуги фулфилмента (расходники)
├── fulfillment-inventory-v2.ts              # V2 складские остатки фулфилмента
├── fulfillment-consumables-v2.ts            # V2 расходники фулфилмента
├── seller-inventory-v2.ts                   # V2 остатки селлеров
├── goods-supply-v2.ts                       # V2 товарные поставки
└── seller-consumables-v2.ts                 # V2 расходники селлеров

/src/graphql/queries/
├── fulfillment-services-v2.ts               # Запросы для услуг фулфилмента
├── seller-consumables-v2.ts                 # Запросы для расходников селлера
└── goods-supply-v2.ts                       # Запросы для товарных поставок

# UI LAYER
/src/app/seller/create/
├── goods/page.tsx                           # Создание товарных поставок
└── consumables/page.tsx                     # Создание расходников

/src/components/supplies/
├── create-suppliers/                        # Форма создания товарных поставок
├── create-consumables-supply-page.tsx       # Форма создания расходников
├── supplies-dashboard.tsx                   # Дашборд поставок
└── multilevel-supplies-table/               # Таблица поставок

/src/components/services/
├── supplies-tab.tsx                         # V2 таб расходников фулфилмента
├── services-tab.tsx                         # V2 таб услуг
└── logistics-tab.tsx                        # V2 таб логистики

🎨 UI АРХИТЕКТУРА

Модульная структура компонентов:

// CreateSuppliersSupplyPage - главный компонент
CreateSuppliersSupplyPage/
├── blocks/                    # UI блоки
   ├── SuppliersBlock.tsx        # Выбор поставщика
   ├── ProductCardsBlock.tsx     # Карточки товаров  
   ├── DetailedCatalogBlock.tsx  # Детальный каталог
   └── CartBlock.tsx             # Корзина
├── hooks/                     # Бизнес-логика
   ├── useSupplierSelection.ts   # Логика выбора поставщика
   ├── useProductCatalog.ts      # Управление каталогом
   ├── useRecipeBuilder.ts       # Построение рецептуры
   └── useSupplyCart.ts          # Логика корзины
└── types/                     # Типы TypeScript
    └── supply-creation.types.ts

Корзина поставки:

interface SupplyCartState {
  selectedGoods: GoodsItem[]               // Товары в корзине
  productRecipes: Record<string, Recipe>   // Рецептуры для каждого товара
  deliverySettings: DeliverySettings       // Настройки доставки
  totalAmount: number                      // Общая сумма заказа
  isFormValid: boolean                     // Валидность формы
  isSubmitting: boolean                    // Состояние отправки
}

interface Recipe {
  selectedServices: string[]               // ID услуг фулфилмента
  fulfillmentConsumables: ConsumableItem[] // Расходники фулфилмента
  sellerConsumables: ConsumableItem[]      // Расходники селлера
  marketplaceCardId?: string               // Карточка WB
}

🔧 РЕЗОЛВЕРЫ И API

V2 API Endpoints:

Queries (чтение данных):

// Для фулфилмента
myFulfillmentServices          // Услуги фулфилмента
myFulfillmentConsumables      // Расходники из каталога
myFulfillmentLogistics        // Логистические маршруты
myFulfillmentSupplies         // Складские остатки (V2)

// Для селлера  
mySellerGoodsSupplies         // Товарные поставки селлера
mySellerConsumableSupplies    // Поставки расходников селлера
mySellerGoodsInventory        // Остатки товаров на складах
mySellerConsumableInventory   // Остатки расходников

// Для поставщика
mySupplierGoodsOrders         // Заказы на товары 
mySupplierConsumableOrders    // Заказы на расходники

// Универсальные
counterpartySupplies          // Каталог расходников партнеров
partnersWithServices          // Партнеры с их услугами

Mutations (изменение данных):

// Создание поставок
createSellerGoodsSupply       // Товарная поставка селлера
createSellerConsumableSupply  // Поставка расходников селлера
createFulfillmentConsumableSupply // Заказ расходников фулфилментом

// Обновление статусов
updateSupplyOrderStatus       // Универсальное обновление статуса
receiveSellerGoodsSupply      // Приемка товаров селлера
receiveConsumableSupply       // Приемка расходников

// Управление каталогом
updateFulfillmentConsumable   // Цены и названия расходников
updateFulfillmentService      // Услуги фулфилмента
updateFulfillmentLogistics    // Логистические маршруты

// Управление инвентарем
updateInventoryStock          // Ручная корректировка остатков
transferInventoryBetweenWarehuses // Перемещение между складами

ПРОИЗВОДИТЕЛЬНОСТЬ И ОПТИМИЗАЦИЯ

🚀 СТРАТЕГИИ ОПТИМИЗАЦИИ

1. Batch операции

// Групповые обновления для больших поставок
export async function batchUpdateInventory(
  operations: InventoryOperation[]
): Promise<void> {
  const operations_grouped = groupBy(operations, 'fulfillmentCenterId')
  
  await Promise.all(
    Object.entries(operations_grouped).map(async ([fulfillmentId, ops]) => {
      return await prisma.$transaction(async (tx) => {
        for (const op of ops) {
          await updateInventorySingle(op, tx)
        }
      })
    })
  )
}

2. Кеширование каталогов

// Redis кеш для часто запрашиваемых каталогов
export class CatalogCache {
  private redis = new Redis(process.env.REDIS_URL)
  
  async getCachedSupplies(fulfillmentId: string): Promise<Supply[]> {
    const cacheKey = `supplies:${fulfillmentId}`
    const cached = await this.redis.get(cacheKey)
    
    if (cached) {
      return JSON.parse(cached)
    }
    
    const supplies = await this.fetchFromDB(fulfillmentId)
    await this.redis.setex(cacheKey, 300, JSON.stringify(supplies)) // 5 мин TTL
    return supplies
  }
}

3. Оптимистичные обновления UI

// useOptimisticInventory.ts
export function useOptimisticInventory() {
  const [optimisticUpdates, setOptimisticUpdates] = useState<OptimisticUpdate[]>([])
  
  const updateOptimistically = (inventoryId: string, stockDelta: number) => {
    setOptimisticUpdates(prev => [
      ...prev,
      {
        inventoryId,
        stockDelta,
        timestamp: Date.now()
      }
    ])
    
    // Откат через 5 секунд, если сервер не подтвердил
    setTimeout(() => revertOptimisticUpdate(inventoryId), 5000)
  }
}

🎨 UI/UX PATTERNS

🧩 КОМПОНЕНТНАЯ АРХИТЕКТУРА

1. Smart Components (Containers)

// Содержат бизнес-логику и состояние
export function SuppliesManagementPage() {
  const { supplies, loading, error } = useSuppliesQuery()
  const { updateStock, isUpdating } = useInventoryMutations()
  
  return (
    <SuppliesTable 
      data={supplies}
      loading={loading}
      onUpdateStock={updateStock}
    />
  )
}

2. Presentation Components

// Чистые UI компоненты без логики
interface SuppliesTableProps {
  data: Supply[]
  loading: boolean
  onUpdateStock: (id: string, quantity: number) => void
}

export function SuppliesTable({ data, loading, onUpdateStock }: SuppliesTableProps) {
  // Только рендеринг, никакой бизнес-логики
}

3. Custom Hooks

// Переиспользуемая бизнес-логика
export function useSupplyCart() {
  const [cartItems, setCartItems] = useState<CartItem[]>([])
  const [totalAmount, setTotalAmount] = useState(0)
  
  const addToCart = useCallback((item: GoodsItem) => {
    setCartItems(prev => [...prev, adaptGoodsToCartItem(item)])
  }, [])
  
  const removeFromCart = useCallback((itemId: string) => {
    setCartItems(prev => prev.filter(item => item.id !== itemId))
  }, [])
  
  return {
    cartItems,
    totalAmount,
    addToCart,
    removeFromCart,
    clearCart: () => setCartItems([]),
    isCartEmpty: cartItems.length === 0
  }
}

🎯 UX PATTERNS

1. Progressive Disclosure

// Постепенное раскрытие сложности
<WizardForm>
  <Step1>Выбор поставщика</Step1>           {/* Простой выбор */}
  <Step2>Выбор товаров</Step2>               {/* Средняя сложность */}
  <Step3>Создание рецептуры</Step3>          {/* Высокая сложность */}
  <Step4>Настройки доставки</Step4>          {/* Финализация */}
</WizardForm>

2. Optimistic Updates

// Мгновенная отзывчивость UI
const handleStockUpdate = async (itemId: string, newStock: number) => {
  // 1. Мгновенно обновляем UI
  updateUIOptimistically(itemId, newStock)
  
  try {
    // 2. Отправляем запрос на сервер
    await updateInventoryStock({ itemId, newStock })
    
    // 3. Подтверждаем успех
    confirmOptimisticUpdate(itemId)
  } catch (error) {
    // 4. Откатываем в случае ошибки
    revertOptimisticUpdate(itemId)
    showErrorToast('Не удалось обновить остаток')
  }
}

3. Real-time Feedback

// Мгновенная обратная связь пользователю
<StockInput
  value={currentStock}
  onChange={(newValue) => {
    updateField('currentStock', newValue)
    
    // Мгновенная валидация
    if (newValue < minStock) {
      showWarning('Остаток ниже минимального')
    }
    
    // Мгновенный расчет
    updateTotalAmount(calculateNewTotal(newValue))
  }}
  validators={[
    (value) => value >= 0 || 'Остаток не может быть отрицательным',
    (value) => value <= maxStock || 'Превышен максимальный остаток'
  ]}
/>

⚠️ ПРОБЛЕМЫ И РИСКИ

🚨 КРИТИЧЕСКИЕ ПРОБЛЕМЫ

1. Потенциальная рассинхронизация данных

// ПРОБЛЕМА: Если автосинхронизация не сработает
if (type === 'INCOMING') {
  try {
    await syncWithFulfillmentConsumable(inventory)
  } catch (syncError) {
    console.error('⚠️ Failed to sync:', syncError)
    // Данные могут рассинхрониться между V1 и V2
  }
}

// РЕШЕНИЕ: Добавить систему retry и мониторинг
const syncWithRetry = async (data, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await syncWithFulfillmentConsumable(data)
      return
    } catch (error) {
      if (attempt === maxRetries) {
        await logSyncFailure(data, error)
        throw error
      }
      await delay(1000 * attempt) // Exponential backoff
    }
  }
}

2. Дублирование бизнес-логики

// ПРОБЛЕМА: Одинаковая логика в разных файлах
// В supplies-dashboard.tsx
const calculateTotalAmount = (items) => items.reduce((sum, item) => sum + item.price * item.quantity, 0)

// В useSupplyCart.ts  
const calculateCartTotal = (cartItems) => cartItems.reduce((total, item) => total + (item.unitPrice * item.quantity), 0)

// РЕШЕНИЕ: Выносить в utils
// /src/lib/utils/calculations.ts
export const calculateSupplyTotal = (items: SupplyItem[]) => {
  return items.reduce((sum, item) => sum + (item.unitPrice * item.quantity), 0)
}

3. Сложность отладки

// ПРОБЛЕМА: Логи разбросаны по разным файлам и системам
console.log('V1 Supply created') // в supplies-dashboard.tsx
console.warn('V2 Inventory updated') // в inventory-management.ts  
console.error('GraphQL error') // в resolvers.ts

// РЕШЕНИЕ: Централизованное логирование
// /src/lib/logging/inventory-logger.ts
export class InventoryLogger {
  static operation(system: 'V1' | 'V2', operation: string, data: any) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      system,
      operation,
      data: JSON.stringify(data),
      sessionId: getCurrentSessionId(),
      userId: getCurrentUserId()
    }
    
    console.log('📦 INVENTORY:', logEntry)
    
    // Отправляем в внешние системы мониторинга
    if (process.env.NODE_ENV === 'production') {
      sendToLogAggregator(logEntry)
    }
  }
}

ПРОИЗВОДИТЕЛЬНОСТЬ

1. N+1 queries проблема

// ПРОБЛЕМА: Запросы в цикле
const supplies = await prisma.supply.findMany()
for (const supply of supplies) {
  supply.organization = await prisma.organization.findUnique({
    where: { id: supply.organizationId }
  })
}

// РЕШЕНИЕ: Include relations
const supplies = await prisma.supply.findMany({
  include: {
    organization: true,
    items: {
      include: {
        product: true
      }
    }
  }
})

2. Большие формы с множественными полями

// ПРОБЛЕМА: Частые ререндеры при изменении формы
const [formData, setFormData] = useState(initialFormData)

// Каждое изменение поля вызывает ререндер всей формы
const updateField = (field, value) => {
  setFormData(prev => ({ ...prev, [field]: value }))
}

// РЕШЕНИЕ: React Hook Form с оптимизациями
const form = useForm({
  defaultValues: initialFormData,
  mode: 'onChange',
  reValidateMode: 'onChange'
})

// Регистрация полей без ререндера родительского компонента
<Input {...form.register('productName')} />

🛠️ РЕКОМЕНДАЦИИ

🎯 КРАТКОСРОЧНЫЕ (1-2 недели)

1. Система мониторинга синхронизации

// /src/lib/monitoring/sync-monitor.ts
export class SyncMonitor {
  static async checkSyncHealth(): Promise<SyncHealth> {
    const v1Count = await prisma.supply.count()
    const v2InventoryCount = await prisma.fulfillmentConsumableInventory.count()
    const v2CatalogCount = await prisma.fulfillmentConsumable.count()
    
    const syncDrift = Math.abs(v2InventoryCount - v2CatalogCount)
    
    return {
      isHealthy: syncDrift < 5, // Допустимое расхождение
      v1Records: v1Count,
      v2InventoryRecords: v2InventoryCount,  
      v2CatalogRecords: v2CatalogCount,
      syncDrift,
      lastSyncCheck: new Date()
    }
  }
}

2. Унификация API для фронтенда

// /src/lib/api/unified-inventory.ts
export class UnifiedInventoryAPI {
  // Единый интерфейс для всех типов инвентаря
  static async getInventory(
    organizationType: OrganizationType,
    organizationId: string,
    filters: InventoryFilters
  ): Promise<InventoryItem[]> {
    switch (organizationType) {
      case 'FULFILLMENT':
        return await this.getFulfillmentInventory(organizationId, filters)
      case 'SELLER':
        return await this.getSellerInventory(organizationId, filters)
      default:
        throw new Error(`Unsupported organization type: ${organizationType}`)
    }
  }
}

3. Валидация данных на всех уровнях

// /src/lib/validation/inventory-validation.ts
export const inventoryValidationSchema = z.object({
  productId: z.string().cuid(),
  quantity: z.number().min(0),
  unitCost: z.number().min(0).optional(),
  type: z.enum(['INCOMING', 'OUTGOING']),
  fulfillmentCenterId: z.string().cuid()
})

export function validateInventoryOperation(data: unknown): InventoryOperation {
  return inventoryValidationSchema.parse(data)
}

🚀 СРЕДНЕСРОЧНЫЕ (1-2 месяца)

1. Микросервисная декомпозиция

// Выделение инвентаря в отдельный сервис
class InventoryService {
  async updateStock(operation: StockOperation): Promise<StockResult>
  async getAnalytics(filters: AnalyticsFilters): Promise<Analytics>
  async validateOperation(operation: StockOperation): Promise<ValidationResult>
  async syncWithExternalSystems(data: SyncData): Promise<void>
}

2. Event-driven архитектура

// Асинхронная обработка через события
export const inventoryEvents = {
  STOCK_UPDATED: 'inventory:stock-updated',
  PRICE_CHANGED: 'inventory:price-changed',
  LOW_STOCK_ALERT: 'inventory:low-stock-alert',
  SYNC_COMPLETED: 'inventory:sync-completed'
}

// Event handlers
EventBus.on(inventoryEvents.STOCK_UPDATED, async (data) => {
  await updateRelatedSystems(data)
  await triggerAnalyticsRecalculation(data.productId)
  await checkLowStockAlerts(data.fulfillmentCenterId)
})

🏗️ ДОЛГОСРОЧНЫЕ (3-6 месяцев)

1. Real-time синхронизация

// WebSocket интеграция для мгновенных обновлений
export class InventoryWebSocket {
  static notify(event: InventoryEvent) {
    const channels = [
      `fulfillment:${event.fulfillmentId}`,
      `seller:${event.sellerId}`,
      `admin:global`
    ]
    
    channels.forEach(channel => {
      webSocketManager.emit(channel, {
        type: event.type,
        data: event.data,
        timestamp: new Date().toISOString()
      })
    })
  }
}

2. Интеграция с внешними системами

// 1С/SAP интеграция
export class ERPIntegration {
  async syncInventoryToERP(inventoryData: InventorySnapshot): Promise<void>
  async importSuppliersFromERP(): Promise<Supplier[]>
  async exportTransactionsToAccounting(period: DateRange): Promise<void>
}

// Маркетплейс интеграция
export class MarketplaceSync {
  async updateWildberriesStock(productId: string, newStock: number): Promise<void>
  async importOzonOrders(): Promise<Order[]>
  async syncPricesAcrossMarketplaces(): Promise<void>
}

📈 МЕТРИКИ И АНАЛИТИКА

📊 KPI СИСТЕМЫ ИНВЕНТАРЯ

interface InventoryKPI {
  // Основные метрики
  totalProducts: number           // Общее количество SKU
  totalStockValue: number        // Общая стоимость остатков  
  avgTurnoverDays: number        // Средняя оборачиваемость
  lowStockItemsCount: number     // Товаров с низким остатком
  
  // Эффективность
  stockoutFrequency: number      // Частота дефицита
  overstockValue: number         // Стоимость излишков
  warehouseUtilization: number   // Заполненность складов
  
  // Финансовые
  avgMarginPercent: number       // Средняя маржинальность
  inventoryROI: number           // ROI по инвентарю
  costOfGoods: number           // Себестоимость товаров
}

🎉 ЗАКЛЮЧЕНИЕ

Система инвентаря SFERA представляет собой сложную, но хорошо структурированную архитектуру с активной миграцией с V1 на V2.

Ключевые достижения:

  • Модульная архитектура с четким разделением ответственности
  • Автосинхронизация между системами V1 и V2
  • Типобезопасная GraphQL схема
  • Производительные UI компоненты с оптимистичными обновлениями

Основные вызовы:

  • ⚠️ Переходный период с дублированием функционала
  • ⚠️ Сложность отладки из-за множественных систем
  • ⚠️ Потенциальная рассинхронизация данных

Следующие шаги:

  1. Завершить миграцию всех компонентов на V2
  2. Добавить мониторинг синхронизации
  3. Оптимизировать производительность запросов
  4. Интегрировать с внешними системами

📊 Статистика системы:

  • Моделей Prisma: 12+ (инвентарь + связанные)
  • GraphQL резолверов: 45+
  • UI компонентов: 25+
  • API endpoints: 30+
  • Бизнес-процессов: 8 основных
  • Статусов поставок: 13 различных