Files
sfera-new/scripts/migrate-v1-to-v2-inventory.ts
2025-08-30 15:51:41 +03:00

377 lines
12 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
/**
* СКРИПТ МИГРАЦИИ V1 → V2 INVENTORY SYSTEM
*
* Безопасно переносит данные из старой системы Supply в новую FulfillmentConsumableInventory
* с возможностью отката и детальной отчетностью
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
interface MigrationStats {
totalV1Records: number
fulfillmentSupplies: number
migratedRecords: number
skippedRecords: number
errors: string[]
warnings: string[]
}
interface V1SupplyRecord {
id: string
name: string
description: string | null
price: any // Decimal
quantity: number
unit: string
category: string | null
status: string
date: Date
supplier: string
minStock: number
currentStock: number
organizationId: string
type: string | null
createdAt: Date
updatedAt: Date
}
const MIGRATION_LOG_PREFIX = '[V1→V2 MIGRATION]'
/**
* Главная функция миграции
*/
async function migrateV1ToV2Inventory(dryRun = true): Promise<MigrationStats> {
const stats: MigrationStats = {
totalV1Records: 0,
fulfillmentSupplies: 0,
migratedRecords: 0,
skippedRecords: 0,
errors: [],
warnings: [],
}
console.log(`${MIGRATION_LOG_PREFIX} Starting migration (DRY RUN: ${dryRun})`)
console.log(`${MIGRATION_LOG_PREFIX} Timestamp: ${new Date().toISOString()}`)
try {
// Получаем все записи из старой системы
const v1Supplies = await prisma.supply.findMany({
include: {
organization: true,
},
orderBy: {
createdAt: 'asc',
},
})
stats.totalV1Records = v1Supplies.length
console.log(`${MIGRATION_LOG_PREFIX} Found ${stats.totalV1Records} V1 Supply records`)
// Фильтруем только расходники фулфилмента
const fulfillmentSupplies = v1Supplies.filter(supply =>
supply.organization?.type === 'FULFILLMENT' &&
supply.type === 'FULFILLMENT_CONSUMABLES'
)
stats.fulfillmentSupplies = fulfillmentSupplies.length
console.log(`${MIGRATION_LOG_PREFIX} Found ${stats.fulfillmentSupplies} Fulfillment consumables to migrate`)
if (stats.fulfillmentSupplies === 0) {
console.log(`${MIGRATION_LOG_PREFIX} No fulfillment supplies to migrate`)
return stats
}
// Создаем соответствующие Product записи для каждого Supply (если еще нет)
const productMap = new Map<string, string>()
for (const supply of fulfillmentSupplies) {
try {
const productId = await ensureProductExists(supply, dryRun)
productMap.set(supply.id, productId)
} catch (error) {
const errorMsg = `Failed to create/find product for supply ${supply.id}: ${error}`
stats.errors.push(errorMsg)
console.error(`${MIGRATION_LOG_PREFIX} ERROR: ${errorMsg}`)
}
}
console.log(`${MIGRATION_LOG_PREFIX} Created/verified ${productMap.size} products`)
// Мигрируем каждую запись Supply → FulfillmentConsumableInventory
for (const supply of fulfillmentSupplies) {
try {
const productId = productMap.get(supply.id)
if (!productId) {
stats.skippedRecords++
continue
}
await migrateSupplyRecord(supply, productId, dryRun)
stats.migratedRecords++
// Логируем прогресс каждые 10 записей
if (stats.migratedRecords % 10 === 0) {
console.log(`${MIGRATION_LOG_PREFIX} Progress: ${stats.migratedRecords}/${fulfillmentSupplies.length}`)
}
} catch (error) {
const errorMsg = `Failed to migrate supply ${supply.id}: ${error}`
stats.errors.push(errorMsg)
console.error(`${MIGRATION_LOG_PREFIX} ERROR: ${errorMsg}`)
stats.skippedRecords++
}
}
} catch (error) {
const errorMsg = `Migration failed: ${error}`
stats.errors.push(errorMsg)
console.error(`${MIGRATION_LOG_PREFIX} CRITICAL ERROR: ${errorMsg}`)
}
// Финальная отчетность
printMigrationReport(stats, dryRun)
return stats
}
/**
* Создает Product запись для Supply, если еще не существует
*/
async function ensureProductExists(supply: V1SupplyRecord, dryRun: boolean): Promise<string> {
// Проверяем есть ли уже Product с таким названием и организацией
const existingProduct = await prisma.product.findFirst({
where: {
organizationId: supply.organizationId,
name: supply.name,
type: 'CONSUMABLE',
},
})
if (existingProduct) {
return existingProduct.id
}
if (dryRun) {
console.log(`${MIGRATION_LOG_PREFIX} [DRY RUN] Would create product: ${supply.name}`)
return `mock-product-id-${supply.id}`
}
// Создаем новый Product на основе Supply данных
const newProduct = await prisma.product.create({
data: {
name: supply.name,
article: `MIGRATED-${supply.id.slice(-8)}`, // Уникальный артикул
description: supply.description || `Мигрировано из V1 Supply ${supply.id}`,
price: supply.price || 0,
unit: supply.unit || 'шт',
category: supply.category || 'Расходники',
type: 'CONSUMABLE',
organizationId: supply.organizationId,
quantity: 0, // В Product quantity означает что-то другое
imageUrl: null,
},
})
console.log(`${MIGRATION_LOG_PREFIX} Created product: ${newProduct.name} (${newProduct.id})`)
return newProduct.id
}
/**
* Мигрирует одну запись Supply в FulfillmentConsumableInventory
*/
async function migrateSupplyRecord(supply: V1SupplyRecord, productId: string, dryRun: boolean): Promise<void> {
if (dryRun) {
console.log(`${MIGRATION_LOG_PREFIX} [DRY RUN] Would migrate: ${supply.name} (${supply.currentStock} units)`)
return
}
// Проверяем не мигрировали ли уже эту запись
const existingInventory = await prisma.fulfillmentConsumableInventory.findUnique({
where: {
fulfillmentCenterId_productId: {
fulfillmentCenterId: supply.organizationId,
productId: productId,
},
},
})
if (existingInventory) {
console.log(`${MIGRATION_LOG_PREFIX} WARNING: Inventory already exists for ${supply.name}, skipping`)
return
}
// Создаем запись в новой системе инвентаря
const inventory = await prisma.fulfillmentConsumableInventory.create({
data: {
fulfillmentCenterId: supply.organizationId,
productId: productId,
// Складские данные из V1
currentStock: supply.currentStock,
minStock: supply.minStock,
maxStock: null, // В V1 не было
reservedStock: 0, // В V1 не было
totalReceived: supply.quantity, // Приблизительно
totalShipped: Math.max(0, supply.quantity - supply.currentStock), // Приблизительно
// Цены
averageCost: parseFloat(supply.price?.toString() || '0'),
resalePrice: null, // В V1 не было
// Метаданные
lastSupplyDate: supply.date,
lastUsageDate: null, // В V1 не было точной информации
notes: `Мигрировано из V1 Supply ${supply.id} (${supply.status})`,
// Временные метки сохраняем из V1
createdAt: supply.createdAt,
updatedAt: supply.updatedAt,
},
})
console.log(`${MIGRATION_LOG_PREFIX} ✅ Migrated: ${supply.name}${inventory.id}`)
}
/**
* Печатает детальный отчет о миграции
*/
function printMigrationReport(stats: MigrationStats, dryRun: boolean): void {
console.log('\n' + '='.repeat(60))
console.log(`${MIGRATION_LOG_PREFIX} MIGRATION REPORT`)
console.log('='.repeat(60))
console.log(`Mode: ${dryRun ? 'DRY RUN' : 'PRODUCTION'}`)
console.log(`Timestamp: ${new Date().toISOString()}`)
console.log('')
console.log('📊 STATISTICS:')
console.log(` Total V1 records found: ${stats.totalV1Records}`)
console.log(` Fulfillment supplies: ${stats.fulfillmentSupplies}`)
console.log(` Successfully migrated: ${stats.migratedRecords}`)
console.log(` Skipped records: ${stats.skippedRecords}`)
console.log(` Errors encountered: ${stats.errors.length}`)
console.log('')
if (stats.errors.length > 0) {
console.log('❌ ERRORS:')
stats.errors.forEach((error, index) => {
console.log(` ${index + 1}. ${error}`)
})
console.log('')
}
if (stats.warnings.length > 0) {
console.log('⚠️ WARNINGS:')
stats.warnings.forEach((warning, index) => {
console.log(` ${index + 1}. ${warning}`)
})
console.log('')
}
if (dryRun) {
console.log('🔄 TO RUN ACTUAL MIGRATION:')
console.log(' npm run migrate:v1-to-v2 --production')
} else {
console.log('✅ MIGRATION COMPLETED!')
console.log('🔄 TO VERIFY RESULTS:')
console.log(' Check FulfillmentConsumableInventory table')
console.log(' Verify counts match expectations')
}
console.log('='.repeat(60))
}
/**
* Функция отката миграции (осторожно!)
*/
async function rollbackMigration(): Promise<void> {
console.log(`${MIGRATION_LOG_PREFIX} ⚠️ STARTING ROLLBACK`)
// Удаляем все мигрированные записи
const deleted = await prisma.fulfillmentConsumableInventory.deleteMany({
where: {
notes: {
contains: 'Мигрировано из V1 Supply'
}
}
})
console.log(`${MIGRATION_LOG_PREFIX} 🗑️ Deleted ${deleted.count} migrated inventory records`)
// Удаляем мигрированные Products (осторожно!)
const deletedProducts = await prisma.product.deleteMany({
where: {
article: {
startsWith: 'MIGRATED-'
}
}
})
console.log(`${MIGRATION_LOG_PREFIX} 🗑️ Deleted ${deletedProducts.count} migrated product records`)
console.log(`${MIGRATION_LOG_PREFIX} ✅ Rollback completed`)
}
/**
* CLI интерфейс
*/
async function main() {
const args = process.argv.slice(2)
const isProduction = args.includes('--production')
const isRollback = args.includes('--rollback')
if (isRollback) {
if (!isProduction) {
console.log('❌ Rollback requires --production flag for safety')
process.exit(1)
}
console.log('⚠️ You are about to rollback the V1→V2 migration!')
console.log('This will DELETE all migrated data!')
console.log('Press Ctrl+C to cancel...')
// Ждем 5 секунд
await new Promise(resolve => setTimeout(resolve, 5000))
await rollbackMigration()
process.exit(0)
}
const dryRun = !isProduction
if (dryRun) {
console.log('🔍 Running in DRY RUN mode (no actual changes)')
console.log('📝 Add --production flag to run actual migration')
} 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 migrateV1ToV2Inventory(dryRun)
if (stats.errors.length > 0) {
console.error(`${MIGRATION_LOG_PREFIX} Migration completed with errors`)
process.exit(1)
}
console.log(`${MIGRATION_LOG_PREFIX} Migration completed successfully`)
process.exit(0)
} catch (error) {
console.error(`${MIGRATION_LOG_PREFIX} Migration failed:`, error)
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
// Запускаем только если скрипт вызван напрямую (ES modules)
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error)
}
export { migrateV1ToV2Inventory, rollbackMigration }