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>
This commit is contained in:
Veronika Smirnova
2025-09-03 23:10:16 +03:00
parent 65fba5d911
commit cdeee82237
35 changed files with 7869 additions and 311 deletions

View File

@ -0,0 +1,1136 @@
# 📦 АРХИТЕКТУРА СИСТЕМЫ ИНВЕНТАРЯ 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 различных