
- ✅ Добавлено поле 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>
41 KiB
41 KiB
📦 АРХИТЕКТУРА СИСТЕМЫ ИНВЕНТАРЯ SFERA
Версия: 2.0
Дата: 2025-09-03
Статус: Активная разработка (миграция V1→V2)
🎯 ОБЗОР СИСТЕМЫ
Система инвентаря SFERA представляет собой комплексную архитектуру для управления складскими остатками, ценообразованием и бизнес-процессами между 4 типами организаций: FULFILLMENT, SELLER, WHOLESALE, LOGIST.
🏗️ АРХИТЕКТУРНЫЕ ПРИНЦИПЫ
- Доменная изоляция - каждая организация видит только свои данные
- Двойная система учета - V1 (legacy) + V2 (современная) с автосинхронизацией
- Модульная структура - независимые резолверы и компоненты
- Типобезопасность - полная типизация 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 компоненты с оптимистичными обновлениями
Основные вызовы:
- ⚠️ Переходный период с дублированием функционала
- ⚠️ Сложность отладки из-за множественных систем
- ⚠️ Потенциальная рассинхронизация данных
Следующие шаги:
- Завершить миграцию всех компонентов на V2
- Добавить мониторинг синхронизации
- Оптимизировать производительность запросов
- Интегрировать с внешними системами
📊 Статистика системы:
- Моделей Prisma: 12+ (инвентарь + связанные)
- GraphQL резолверов: 45+
- UI компонентов: 25+
- API endpoints: 30+
- Бизнес-процессов: 8 основных
- Статусов поставок: 13 различных