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:
Veronika Smirnova
2025-09-03 23:10:16 +03:00
parent 65fba5d911
commit cdeee82237
35 changed files with 7869 additions and 311 deletions

View File

@ -12,6 +12,7 @@ import { WildberriesService } from '@/services/wildberries-service'
import { fulfillmentConsumableV2Queries as fulfillmentConsumableV2QueriesRestored, fulfillmentConsumableV2Mutations as fulfillmentConsumableV2MutationsRestored } from './resolvers/fulfillment-consumables-v2-restored'
import { fulfillmentInventoryV2Queries } from './resolvers/fulfillment-inventory-v2'
import { fulfillmentServicesQueries, fulfillmentServicesMutations } from './resolvers/fulfillment-services-v2'
import { sellerGoodsQueries, sellerGoodsMutations } from './resolvers/goods-supply-v2'
import { logisticsConsumableV2Queries, logisticsConsumableV2Mutations } from './resolvers/logistics-consumables-v2'
import { sellerConsumableQueries, sellerConsumableMutations } from './resolvers/seller-consumables'
@ -2170,40 +2171,39 @@ export const resolvers = {
throw new GraphQLError('Расходники доступны только у фулфилмент центров')
}
// Получаем расходники из V2 инвентаря фулфилмента с правильными ценами
const inventoryItems = await prisma.fulfillmentConsumableInventory.findMany({
// V2 СИСТЕМА А: Получаем расходники из каталога услуг фулфилмента
const consumables = await prisma.fulfillmentConsumable.findMany({
where: {
fulfillmentCenterId: args.organizationId,
currentStock: { gt: 0 }, // Только те, что есть в наличии
resalePrice: { not: null }, // Только те, у которых установлена цена
fulfillmentId: args.organizationId,
isAvailable: true,
pricePerUnit: { gt: 0 }, // Только с установленными ценами для селлеров
},
include: {
product: true,
fulfillmentCenter: true,
fulfillment: true,
},
orderBy: { lastSupplyDate: 'desc' },
orderBy: { sortOrder: 'asc' },
})
console.warn('🔥 COUNTERPARTY SUPPLIES - V2 FORMAT:', {
console.warn('🔥 COUNTERPARTY SUPPLIES - V2 СИСТЕМА А:', {
organizationId: args.organizationId,
itemsCount: inventoryItems.length,
itemsWithPrices: inventoryItems.filter(item => item.resalePrice).length,
consumablesCount: consumables.length,
consumablesWithPrices: consumables.filter(c => c.pricePerUnit > 0).length,
})
// Преобразуем V2 формат в формат старого Supply для обратной совместимости
return inventoryItems.map((item) => ({
id: item.id,
name: item.product.name,
description: item.product.description || '',
price: item.resalePrice ? parseFloat(item.resalePrice.toString()) : 0, // Цена перепродажи из V2
quantity: item.currentStock, // Текущий остаток
unit: 'шт', // TODO: добавить unit в Product модель
// Преобразуем V2 Система А в формат V1 для обратной совместимости
return consumables.map((consumable) => ({
id: consumable.id,
name: consumable.nameForSeller || consumable.name, // Используем nameForSeller если установлено
description: consumable.description || '',
price: parseFloat(consumable.pricePerUnit.toString()), // Цена для селлеров из Системы А
quantity: consumable.currentStock, // Текущий остаток
unit: consumable.unit, // Единица измерения
category: 'CONSUMABLE',
status: 'AVAILABLE',
imageUrl: item.product.mainImage,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
organization: item.fulfillmentCenter,
imageUrl: consumable.imageUrl,
createdAt: consumable.createdAt,
updatedAt: consumable.updatedAt,
organization: consumable.fulfillment,
}))
},
@ -2916,6 +2916,9 @@ export const resolvers = {
// Новая система складских остатков V2 (заменяет старый myFulfillmentSupplies)
...fulfillmentInventoryV2Queries,
// V2 система услуг фулфилмента (включая myFulfillmentConsumables)
...fulfillmentServicesQueries,
// V2 система складских остатков расходников селлера
...sellerInventoryV2Queries,
@ -4861,6 +4864,8 @@ export const resolvers = {
},
context: Context,
) => {
console.warn('🔥 UPDATE_SUPPLY_PRICE called with args:', args)
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
@ -4882,48 +4887,46 @@ export const resolvers = {
}
try {
// Находим и обновляем расходник в V2 таблице FulfillmentConsumableInventory
const existingInventoryItem = await prisma.fulfillmentConsumableInventory.findFirst({
// V2 СИСТЕМА А: Находим и обновляем расходник в каталоге услуг фулфилмента
const existingConsumable = await prisma.fulfillmentConsumable.findFirst({
where: {
id: args.id,
fulfillmentCenterId: currentUser.organization.id,
fulfillmentId: currentUser.organization.id,
},
include: {
product: true,
fulfillmentCenter: true,
fulfillment: true,
},
})
if (!existingInventoryItem) {
throw new GraphQLError('Расходник не найден в инвентаре')
if (!existingConsumable) {
throw new GraphQLError('Расходник не найден в каталоге услуг')
}
const updatedInventoryItem = await prisma.fulfillmentConsumableInventory.update({
const updatedConsumable = await prisma.fulfillmentConsumable.update({
where: { id: args.id },
data: {
resalePrice: args.input.pricePerUnit, // Обновляем цену перепродажи в V2
pricePerUnit: args.input.pricePerUnit || 0, // Обновляем цену для селлеров в V2 Система А
updatedAt: new Date(),
},
include: {
product: true,
fulfillmentCenter: true,
fulfillment: true,
},
})
// Преобразуем V2 данные в формат для GraphQL (аналогично mySupplies resolver)
// Преобразуем V2 данные в формат для GraphQL
const transformedSupply = {
id: updatedInventoryItem.id,
name: updatedInventoryItem.product.name,
description: updatedInventoryItem.product.description || '',
pricePerUnit: updatedInventoryItem.resalePrice ? parseFloat(updatedInventoryItem.resalePrice.toString()) : null,
unit: 'шт', // TODO: добавить unit в Product модель
imageUrl: updatedInventoryItem.product.mainImage,
warehouseStock: updatedInventoryItem.currentStock,
isAvailable: updatedInventoryItem.currentStock > 0,
warehouseConsumableId: updatedInventoryItem.id,
createdAt: updatedInventoryItem.createdAt,
updatedAt: updatedInventoryItem.updatedAt,
organization: updatedInventoryItem.fulfillmentCenter,
id: updatedConsumable.id,
name: updatedConsumable.name,
description: updatedConsumable.description || '',
pricePerUnit: parseFloat(updatedConsumable.pricePerUnit.toString()),
unit: updatedConsumable.unit,
imageUrl: updatedConsumable.imageUrl,
warehouseStock: updatedConsumable.currentStock,
isAvailable: updatedConsumable.isAvailable,
warehouseConsumableId: updatedConsumable.id,
createdAt: updatedConsumable.createdAt,
updatedAt: updatedConsumable.updatedAt,
organization: updatedConsumable.fulfillment,
}
console.warn('🔥 V2 SUPPLY PRICE UPDATED:', {
@ -4978,34 +4981,33 @@ export const resolvers = {
}
try {
const updatedInventoryItem = await prisma.fulfillmentConsumableInventory.update({
const updatedConsumable = await prisma.fulfillmentConsumable.update({
where: {
id: args.id,
fulfillmentCenterId: currentUser.organization.id,
fulfillmentId: currentUser.organization.id,
},
data: {
resalePrice: args.input.pricePerUnit,
pricePerUnit: args.input.pricePerUnit || 0,
updatedAt: new Date(),
},
include: {
product: true,
fulfillmentCenter: true,
fulfillment: true,
},
})
const transformedItem = {
id: updatedInventoryItem.id,
name: updatedInventoryItem.product.name,
description: updatedInventoryItem.product.description || '',
pricePerUnit: updatedInventoryItem.resalePrice ? parseFloat(updatedInventoryItem.resalePrice.toString()) : null,
unit: 'шт',
imageUrl: updatedInventoryItem.product.mainImage,
warehouseStock: updatedInventoryItem.currentStock,
isAvailable: updatedInventoryItem.currentStock > 0,
warehouseConsumableId: updatedInventoryItem.id,
createdAt: updatedInventoryItem.createdAt,
updatedAt: updatedInventoryItem.updatedAt,
organization: updatedInventoryItem.fulfillmentCenter,
id: updatedConsumable.id,
name: updatedConsumable.name,
description: updatedConsumable.description || '',
pricePerUnit: parseFloat(updatedConsumable.pricePerUnit.toString()),
unit: updatedConsumable.unit,
imageUrl: updatedConsumable.imageUrl,
warehouseStock: updatedConsumable.currentStock,
isAvailable: updatedConsumable.isAvailable,
warehouseConsumableId: updatedConsumable.id,
createdAt: updatedConsumable.createdAt,
updatedAt: updatedConsumable.updatedAt,
organization: updatedConsumable.fulfillment,
}
console.warn('🔥 V2 FULFILLMENT INVENTORY PRICE UPDATED:', {
@ -10310,6 +10312,9 @@ resolvers.Mutation = {
// V2 mutations для поставок расходников селлера
...sellerConsumableMutations,
// V2 mutations для услуг фулфилмента
...fulfillmentServicesMutations,
// V2 mutations для товарных поставок селлера
...sellerGoodsMutations,
}