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

1136 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📦 АРХИТЕКТУРА СИСТЕМЫ ИНВЕНТАРЯ 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** - Складские остатки фулфилмента
```prisma
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** - Каталог услуг фулфилмента
```prisma
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** - Складские остатки товаров селлера
```prisma
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])
}
```
---
## 🔄 БИЗНЕС-ПРОЦЕССЫ
### 📋 ПОТОК ТОВАРОВ: СЕЛЛЕР → ПОСТАВЩИК → ФУЛФИЛМЕНТ
```mermaid
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 Компоненты:**
```typescript
// 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 Мутация:**
```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)
**Резолвер:**
```typescript
// 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: ПРИЕМКА НА СКЛАДЕ (Фулфилмент)
**Мутация приемки:**
```typescript
// 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 } ]
)
}
```
**Управление инвентарем:**
```typescript
// 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)**
```typescript
// Рассчитывается автоматически методом взвешенной средней
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)**
```typescript
// Устанавливается фулфилментом вручную
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. МАРЖА И АНАЛИТИКА**
```typescript
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
### 🎯 СТРАТЕГИЯ СИНХРОНИЗАЦИИ
```typescript
// 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)
}
}
```
### 📊 ТРИГГЕРЫ СИНХРОНИЗАЦИИ
```typescript
// Когда срабатывает автосинхронизация:
1. **Поступление товаров** updateInventory(type: 'INCOMING')
├── Обновляет FulfillmentConsumableInventory
└── Создает/обновляет FulfillmentConsumable
2. **Использование товаров** updateInventory(type: 'OUTGOING')
├── Обновляет FulfillmentConsumableInventory
└── Обновляет FulfillmentConsumable.currentStock
3. **Изменение цен** updateFulfillmentConsumable()
├── Обновляет FulfillmentConsumable.pricePerUnit
└── Синхронизирует с FulfillmentConsumableInventory.resalePrice
4. **Одноразовая миграция** syncInventoryToCatalog()
└── Переносит все данные из Системы B в Систему A
```
---
## 🔧 ТЕХНИЧЕСКИЕ ДЕТАЛИ
### 📁 СТРУКТУРА ФАЙЛОВ
```bash
# 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 АРХИТЕКТУРА
#### **Модульная структура компонентов:**
```typescript
// 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
```
#### **Корзина поставки:**
```typescript
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 (чтение данных):**
```typescript
// Для фулфилмента
myFulfillmentServices // Услуги фулфилмента
myFulfillmentConsumables // Расходники из каталога
myFulfillmentLogistics // Логистические маршруты
myFulfillmentSupplies // Складские остатки (V2)
// Для селлера
mySellerGoodsSupplies // Товарные поставки селлера
mySellerConsumableSupplies // Поставки расходников селлера
mySellerGoodsInventory // Остатки товаров на складах
mySellerConsumableInventory // Остатки расходников
// Для поставщика
mySupplierGoodsOrders // Заказы на товары
mySupplierConsumableOrders // Заказы на расходники
// Универсальные
counterpartySupplies // Каталог расходников партнеров
partnersWithServices // Партнеры с их услугами
```
**Mutations (изменение данных):**
```typescript
// Создание поставок
createSellerGoodsSupply // Товарная поставка селлера
createSellerConsumableSupply // Поставка расходников селлера
createFulfillmentConsumableSupply // Заказ расходников фулфилментом
// Обновление статусов
updateSupplyOrderStatus // Универсальное обновление статуса
receiveSellerGoodsSupply // Приемка товаров селлера
receiveConsumableSupply // Приемка расходников
// Управление каталогом
updateFulfillmentConsumable // Цены и названия расходников
updateFulfillmentService // Услуги фулфилмента
updateFulfillmentLogistics // Логистические маршруты
// Управление инвентарем
updateInventoryStock // Ручная корректировка остатков
transferInventoryBetweenWarehuses // Перемещение между складами
```
---
## ⚡ ПРОИЗВОДИТЕЛЬНОСТЬ И ОПТИМИЗАЦИЯ
### 🚀 СТРАТЕГИИ ОПТИМИЗАЦИИ
#### **1. Batch операции**
```typescript
// Групповые обновления для больших поставок
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. Кеширование каталогов**
```typescript
// 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**
```typescript
// 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)**
```typescript
// Содержат бизнес-логику и состояние
export function SuppliesManagementPage() {
const { supplies, loading, error } = useSuppliesQuery()
const { updateStock, isUpdating } = useInventoryMutations()
return (
<SuppliesTable
data={supplies}
loading={loading}
onUpdateStock={updateStock}
/>
)
}
```
#### **2. Presentation Components**
```typescript
// Чистые UI компоненты без логики
interface SuppliesTableProps {
data: Supply[]
loading: boolean
onUpdateStock: (id: string, quantity: number) => void
}
export function SuppliesTable({ data, loading, onUpdateStock }: SuppliesTableProps) {
// Только рендеринг, никакой бизнес-логики
}
```
#### **3. Custom Hooks**
```typescript
// Переиспользуемая бизнес-логика
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**
```typescript
// Постепенное раскрытие сложности
<WizardForm>
<Step1>Выбор поставщика</Step1> {/* Простой выбор */}
<Step2>Выбор товаров</Step2> {/* Средняя сложность */}
<Step3>Создание рецептуры</Step3> {/* Высокая сложность */}
<Step4>Настройки доставки</Step4> {/* Финализация */}
</WizardForm>
```
#### **2. Optimistic Updates**
```typescript
// Мгновенная отзывчивость 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**
```typescript
// Мгновенная обратная связь пользователю
<StockInput
value={currentStock}
onChange={(newValue) => {
updateField('currentStock', newValue)
// Мгновенная валидация
if (newValue < minStock) {
showWarning('Остаток ниже минимального')
}
// Мгновенный расчет
updateTotalAmount(calculateNewTotal(newValue))
}}
validators={[
(value) => value >= 0 || 'Остаток не может быть отрицательным',
(value) => value <= maxStock || 'Превышен максимальный остаток'
]}
/>
```
---
## ⚠️ ПРОБЛЕМЫ И РИСКИ
### 🚨 КРИТИЧЕСКИЕ ПРОБЛЕМЫ
#### **1. Потенциальная рассинхронизация данных**
```typescript
// ПРОБЛЕМА: Если автосинхронизация не сработает
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. Дублирование бизнес-логики**
```typescript
// ПРОБЛЕМА: Одинаковая логика в разных файлах
// В 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. Сложность отладки**
```typescript
// ПРОБЛЕМА: Логи разбросаны по разным файлам и системам
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 проблема**
```typescript
// ПРОБЛЕМА: Запросы в цикле
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. Большие формы с множественными полями**
```typescript
// ПРОБЛЕМА: Частые ререндеры при изменении формы
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. Система мониторинга синхронизации**
```typescript
// /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 для фронтенда**
```typescript
// /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. Валидация данных на всех уровнях**
```typescript
// /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. Микросервисная декомпозиция**
```typescript
// Выделение инвентаря в отдельный сервис
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 архитектура**
```typescript
// Асинхронная обработка через события
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 синхронизация**
```typescript
// 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. Интеграция с внешними системами**
```typescript
// 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 СИСТЕМЫ ИНВЕНТАРЯ
```typescript
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 различных