Files
sfera-new/scripts/sync-inventory-to-catalog.ts
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

235 lines
7.5 KiB
TypeScript
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.

#!/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 }