
- ✅ Добавлено поле 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>
235 lines
7.5 KiB
TypeScript
235 lines
7.5 KiB
TypeScript
#!/usr/bin/env tsx
|
||
/**
|
||
* СКРИПТ СИНХРОНИЗАЦИИ СИСТЕМА B → СИСТЕМА A
|
||
*
|
||
* Синхронизирует существующие данные из FulfillmentConsumableInventory
|
||
* в FulfillmentConsumable для обеспечения доступности расходников в каталоге
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
interface SyncStats {
|
||
totalInventoryItems: number
|
||
existingCatalogItems: number
|
||
createdCatalogItems: number
|
||
updatedCatalogItems: number
|
||
errors: string[]
|
||
}
|
||
|
||
const SYNC_LOG_PREFIX = '[INVENTORY→CATALOG SYNC]'
|
||
|
||
/**
|
||
* Главная функция синхронизации
|
||
*/
|
||
async function syncInventoryToCatalog(dryRun = true): Promise<SyncStats> {
|
||
const stats: SyncStats = {
|
||
totalInventoryItems: 0,
|
||
existingCatalogItems: 0,
|
||
createdCatalogItems: 0,
|
||
updatedCatalogItems: 0,
|
||
errors: [],
|
||
}
|
||
|
||
console.warn(`${SYNC_LOG_PREFIX} Starting sync (DRY RUN: ${dryRun})`)
|
||
console.warn(`${SYNC_LOG_PREFIX} Timestamp: ${new Date().toISOString()}`)
|
||
|
||
try {
|
||
// Получаем все записи из Системы B (склад)
|
||
const inventoryItems = await prisma.fulfillmentConsumableInventory.findMany({
|
||
include: {
|
||
product: true,
|
||
fulfillmentCenter: true,
|
||
},
|
||
orderBy: {
|
||
createdAt: 'asc',
|
||
},
|
||
})
|
||
|
||
stats.totalInventoryItems = inventoryItems.length
|
||
console.warn(`${SYNC_LOG_PREFIX} Found ${stats.totalInventoryItems} inventory items`)
|
||
|
||
if (stats.totalInventoryItems === 0) {
|
||
console.warn(`${SYNC_LOG_PREFIX} No inventory items to sync`)
|
||
return stats
|
||
}
|
||
|
||
// Синхронизируем каждую запись
|
||
for (const inventoryItem of inventoryItems) {
|
||
try {
|
||
await syncInventoryItem(inventoryItem, stats, dryRun)
|
||
|
||
// Логируем прогресс каждые 5 записей
|
||
if ((stats.createdCatalogItems + stats.updatedCatalogItems) % 5 === 0) {
|
||
console.warn(`${SYNC_LOG_PREFIX} Progress: ${stats.createdCatalogItems + stats.updatedCatalogItems}/${inventoryItems.length}`)
|
||
}
|
||
|
||
} catch (error) {
|
||
const errorMsg = `Failed to sync inventory item ${inventoryItem.id}: ${error}`
|
||
stats.errors.push(errorMsg)
|
||
console.error(`${SYNC_LOG_PREFIX} ERROR: ${errorMsg}`)
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
const errorMsg = `Sync failed: ${error}`
|
||
stats.errors.push(errorMsg)
|
||
console.error(`${SYNC_LOG_PREFIX} CRITICAL ERROR: ${errorMsg}`)
|
||
}
|
||
|
||
// Финальная отчетность
|
||
printSyncReport(stats, dryRun)
|
||
return stats
|
||
}
|
||
|
||
/**
|
||
* Синхронизирует одну запись из склада в каталог
|
||
*/
|
||
async function syncInventoryItem(inventoryItem: any, stats: SyncStats, dryRun: boolean): Promise<void> {
|
||
const fulfillmentCenterId = inventoryItem.fulfillmentCenterId
|
||
const productName = inventoryItem.product.name
|
||
|
||
// Проверяем существует ли запись в каталоге
|
||
const existingCatalogItem = await prisma.fulfillmentConsumable.findFirst({
|
||
where: {
|
||
fulfillmentId: fulfillmentCenterId,
|
||
name: productName,
|
||
},
|
||
})
|
||
|
||
if (existingCatalogItem) {
|
||
stats.existingCatalogItems++
|
||
|
||
if (dryRun) {
|
||
console.warn(`${SYNC_LOG_PREFIX} [DRY RUN] Would update: ${productName} ` +
|
||
`(stock: ${inventoryItem.currentStock})`)
|
||
return
|
||
}
|
||
|
||
// Обновляем существующую запись в каталоге
|
||
await prisma.fulfillmentConsumable.update({
|
||
where: { id: existingCatalogItem.id },
|
||
data: {
|
||
currentStock: inventoryItem.currentStock,
|
||
minStock: inventoryItem.minStock,
|
||
isAvailable: inventoryItem.currentStock > 0,
|
||
inventoryId: inventoryItem.id,
|
||
updatedAt: new Date(),
|
||
},
|
||
})
|
||
|
||
stats.updatedCatalogItems++
|
||
console.warn(`${SYNC_LOG_PREFIX} ✅ Updated catalog: ${productName}`)
|
||
} else {
|
||
if (dryRun) {
|
||
console.warn(`${SYNC_LOG_PREFIX} [DRY RUN] Would create: ${productName} ` +
|
||
`(stock: ${inventoryItem.currentStock})`)
|
||
return
|
||
}
|
||
|
||
// Создаем новую запись в каталоге
|
||
await prisma.fulfillmentConsumable.create({
|
||
data: {
|
||
fulfillmentId: fulfillmentCenterId,
|
||
inventoryId: inventoryItem.id,
|
||
name: inventoryItem.product.name,
|
||
article: inventoryItem.product.article || '',
|
||
pricePerUnit: 0, // Цену фулфилмент устанавливает вручную
|
||
unit: inventoryItem.product.unit || 'шт',
|
||
minStock: inventoryItem.minStock,
|
||
currentStock: inventoryItem.currentStock,
|
||
isAvailable: inventoryItem.currentStock > 0,
|
||
imageUrl: inventoryItem.product.imageUrl,
|
||
sortOrder: 0,
|
||
},
|
||
})
|
||
|
||
stats.createdCatalogItems++
|
||
console.warn(`${SYNC_LOG_PREFIX} ✅ Created catalog: ${productName}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Печатает детальный отчет о синхронизации
|
||
*/
|
||
function printSyncReport(stats: SyncStats, dryRun: boolean): void {
|
||
console.log('\n' + '='.repeat(60))
|
||
console.warn(`${SYNC_LOG_PREFIX} SYNC REPORT`)
|
||
console.log('='.repeat(60))
|
||
console.warn(`Mode: ${dryRun ? 'DRY RUN' : 'PRODUCTION'}`)
|
||
console.warn(`Timestamp: ${new Date().toISOString()}`)
|
||
console.log('')
|
||
console.log('📊 STATISTICS:')
|
||
console.warn(` Inventory items found: ${stats.totalInventoryItems}`)
|
||
console.warn(` Existing catalog items: ${stats.existingCatalogItems}`)
|
||
console.warn(` Created catalog items: ${stats.createdCatalogItems}`)
|
||
console.warn(` Updated catalog items: ${stats.updatedCatalogItems}`)
|
||
console.warn(` Errors encountered: ${stats.errors.length}`)
|
||
console.log('')
|
||
|
||
if (stats.errors.length > 0) {
|
||
console.log('❌ ERRORS:')
|
||
stats.errors.forEach((error, index) => {
|
||
console.warn(` ${index + 1}. ${error}`)
|
||
})
|
||
console.log('')
|
||
}
|
||
|
||
if (dryRun) {
|
||
console.log('🔄 TO RUN ACTUAL SYNC:')
|
||
console.log(' node scripts/sync-inventory-to-catalog.ts --production')
|
||
} else {
|
||
console.log('✅ SYNC COMPLETED!')
|
||
console.log('🔄 TO VERIFY RESULTS:')
|
||
console.log(' Check FulfillmentConsumable table')
|
||
console.log(' Refresh http://localhost:3000/fulfillment/services/consumables')
|
||
}
|
||
|
||
console.log('='.repeat(60))
|
||
}
|
||
|
||
/**
|
||
* CLI интерфейс
|
||
*/
|
||
async function main() {
|
||
const args = process.argv.slice(2)
|
||
const isProduction = args.includes('--production')
|
||
const dryRun = !isProduction
|
||
|
||
if (dryRun) {
|
||
console.log('🔍 Running in DRY RUN mode (no actual changes)')
|
||
console.log('📝 Add --production flag to run actual sync')
|
||
} else {
|
||
console.log('⚠️ Running in PRODUCTION mode (will make changes)')
|
||
console.log('Press Ctrl+C to cancel...')
|
||
|
||
// Ждем 3 секунды в production режиме
|
||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||
}
|
||
|
||
try {
|
||
const stats = await syncInventoryToCatalog(dryRun)
|
||
|
||
if (stats.errors.length > 0) {
|
||
console.error(`${SYNC_LOG_PREFIX} Sync completed with errors`)
|
||
process.exit(1)
|
||
}
|
||
|
||
console.warn(`${SYNC_LOG_PREFIX} Sync completed successfully`)
|
||
process.exit(0)
|
||
|
||
} catch (error) {
|
||
console.error(`${SYNC_LOG_PREFIX} Sync failed:`, error)
|
||
process.exit(1)
|
||
} finally {
|
||
await prisma.$disconnect()
|
||
}
|
||
}
|
||
|
||
// Запускаем только если скрипт вызван напрямую
|
||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||
main().catch(console.error)
|
||
}
|
||
|
||
export { syncInventoryToCatalog } |