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:
206
scripts/seed-v2-test-data.js
Normal file
206
scripts/seed-v2-test-data.js
Normal file
@ -0,0 +1,206 @@
|
||||
// Скрипт для создания тестовых данных V2
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function seedV2TestData() {
|
||||
try {
|
||||
console.warn('🌱 Создание тестовых данных для V2 системы...')
|
||||
|
||||
// Найдем фулфилмент организацию
|
||||
const fulfillment = await prisma.organization.findFirst({
|
||||
where: { type: 'FULFILLMENT' }
|
||||
})
|
||||
|
||||
if (!fulfillment) {
|
||||
throw new Error('❌ Фулфилмент организация не найдена')
|
||||
}
|
||||
|
||||
console.warn(`✅ Найдена фулфилмент организация: ${fulfillment.name} (${fulfillment.id})`)
|
||||
|
||||
// Создаем тестовые услуги
|
||||
console.warn('📋 Создание тестовых услуг...')
|
||||
const services = await Promise.all([
|
||||
prisma.fulfillmentService.upsert({
|
||||
where: { id: 'test-service-1' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-service-1',
|
||||
fulfillmentId: fulfillment.id,
|
||||
name: 'Упаковка товаров',
|
||||
description: 'Профессиональная упаковка с пузырчатой пленкой',
|
||||
price: 50.00,
|
||||
unit: 'шт',
|
||||
isActive: true,
|
||||
sortOrder: 1
|
||||
}
|
||||
}),
|
||||
prisma.fulfillmentService.upsert({
|
||||
where: { id: 'test-service-2' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-service-2',
|
||||
fulfillmentId: fulfillment.id,
|
||||
name: 'Маркировка товаров',
|
||||
description: 'Наклейка штрих-кодов и этикеток',
|
||||
price: 15.00,
|
||||
unit: 'шт',
|
||||
isActive: true,
|
||||
sortOrder: 2
|
||||
}
|
||||
}),
|
||||
prisma.fulfillmentService.upsert({
|
||||
where: { id: 'test-service-3' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-service-3',
|
||||
fulfillmentId: fulfillment.id,
|
||||
name: 'Фотосъемка товаров',
|
||||
description: 'Профессиональная предметная съемка',
|
||||
price: 200.00,
|
||||
unit: 'шт',
|
||||
isActive: true,
|
||||
sortOrder: 3
|
||||
}
|
||||
})
|
||||
])
|
||||
console.warn(`✅ Создано ${services.length} тестовых услуг`)
|
||||
|
||||
// Создаем тестовые расходники
|
||||
console.warn('📦 Создание тестовых расходников...')
|
||||
const consumables = await Promise.all([
|
||||
prisma.fulfillmentConsumable.upsert({
|
||||
where: { id: 'test-consumable-1' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-consumable-1',
|
||||
fulfillmentId: fulfillment.id,
|
||||
name: 'Коробки картонные 20x15x10',
|
||||
article: 'BOX-001',
|
||||
description: 'Стандартные коробки для упаковки мелких товаров',
|
||||
pricePerUnit: 25.50,
|
||||
unit: 'шт',
|
||||
minStock: 20,
|
||||
currentStock: 150,
|
||||
isAvailable: true,
|
||||
sortOrder: 1
|
||||
}
|
||||
}),
|
||||
prisma.fulfillmentConsumable.upsert({
|
||||
where: { id: 'test-consumable-2' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-consumable-2',
|
||||
fulfillmentId: fulfillment.id,
|
||||
name: 'Пузырчатая пленка',
|
||||
article: 'BUBBLE-001',
|
||||
description: 'Защитная пленка для хрупких товаров',
|
||||
pricePerUnit: 12.30,
|
||||
unit: 'м',
|
||||
minStock: 50,
|
||||
currentStock: 500,
|
||||
isAvailable: true,
|
||||
sortOrder: 2
|
||||
}
|
||||
}),
|
||||
prisma.fulfillmentConsumable.upsert({
|
||||
where: { id: 'test-consumable-3' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-consumable-3',
|
||||
fulfillmentId: fulfillment.id,
|
||||
name: 'Скотч упаковочный',
|
||||
article: 'TAPE-001',
|
||||
description: 'Прозрачный упаковочный скотч 48мм',
|
||||
pricePerUnit: 8.75,
|
||||
unit: 'шт',
|
||||
minStock: 10,
|
||||
currentStock: 75,
|
||||
isAvailable: true,
|
||||
sortOrder: 3
|
||||
}
|
||||
}),
|
||||
prisma.fulfillmentConsumable.upsert({
|
||||
where: { id: 'test-consumable-4' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-consumable-4',
|
||||
fulfillmentId: fulfillment.id,
|
||||
name: 'Этикетки самоклеящиеся',
|
||||
article: 'LABEL-001',
|
||||
description: 'Белые этикетки для маркировки',
|
||||
pricePerUnit: 5.20,
|
||||
unit: 'лист',
|
||||
minStock: 5,
|
||||
currentStock: 0,
|
||||
isAvailable: false,
|
||||
sortOrder: 4
|
||||
}
|
||||
})
|
||||
])
|
||||
console.warn(`✅ Создано ${consumables.length} тестовых расходников`)
|
||||
|
||||
// Создаем тестовые логистические маршруты
|
||||
console.warn('🚚 Создание тестовых логистических маршрутов...')
|
||||
const logistics = await Promise.all([
|
||||
prisma.fulfillmentLogistics.upsert({
|
||||
where: { id: 'test-logistics-1' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-logistics-1',
|
||||
fulfillmentId: fulfillment.id,
|
||||
fromLocation: 'Москва',
|
||||
toLocation: 'Санкт-Петербург',
|
||||
priceUnder1m3: 800.00,
|
||||
priceOver1m3: 1200.00,
|
||||
estimatedDays: 2,
|
||||
description: 'Экспресс доставка до двери',
|
||||
isActive: true,
|
||||
sortOrder: 1
|
||||
}
|
||||
}),
|
||||
prisma.fulfillmentLogistics.upsert({
|
||||
where: { id: 'test-logistics-2' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-logistics-2',
|
||||
fulfillmentId: fulfillment.id,
|
||||
fromLocation: 'Москва',
|
||||
toLocation: 'Казань',
|
||||
priceUnder1m3: 600.00,
|
||||
priceOver1m3: 900.00,
|
||||
estimatedDays: 3,
|
||||
description: 'Стандартная доставка',
|
||||
isActive: true,
|
||||
sortOrder: 2
|
||||
}
|
||||
}),
|
||||
prisma.fulfillmentLogistics.upsert({
|
||||
where: { id: 'test-logistics-3' },
|
||||
update: {},
|
||||
create: {
|
||||
id: 'test-logistics-3',
|
||||
fulfillmentId: fulfillment.id,
|
||||
fromLocation: 'Санкт-Петербург',
|
||||
toLocation: 'Новосибирск',
|
||||
priceUnder1m3: 1500.00,
|
||||
priceOver1m3: 2200.00,
|
||||
estimatedDays: 5,
|
||||
description: 'Межрегиональная доставка',
|
||||
isActive: true,
|
||||
sortOrder: 3
|
||||
}
|
||||
})
|
||||
])
|
||||
console.warn(`✅ Создано ${logistics.length} тестовых логистических маршрутов`)
|
||||
|
||||
console.warn('🎉 Все тестовые данные V2 созданы успешно!')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при создании тестовых данных:', error)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
seedV2TestData()
|
235
scripts/sync-inventory-to-catalog.ts
Normal file
235
scripts/sync-inventory-to-catalog.ts
Normal file
@ -0,0 +1,235 @@
|
||||
#!/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 }
|
Reference in New Issue
Block a user