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

@ -154,6 +154,230 @@ export const fulfillmentConsumableV2Queries = {
},
}
// =============================================================================
// 🔄 МУТАЦИИ ПОСТАВЩИКА ДЛЯ FULFILLMENT CONSUMABLE SUPPLY
// =============================================================================
const supplierApproveConsumableSupply = async (
_: unknown,
args: { id: string },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут одобрять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'PENDING') {
throw new GraphQLError('Поставку можно одобрить только в статусе PENDING')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'SUPPLIER_APPROVED',
supplierApprovedAt: new Date(),
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
return {
success: true,
message: 'Поставка одобрена успешно',
order: updatedSupply,
}
} catch (error) {
console.error('Error approving fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка одобрения поставки',
order: null,
}
}
}
const supplierRejectConsumableSupply = async (
_: unknown,
args: { id: string; reason?: string },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут отклонять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (supply.status !== 'PENDING') {
throw new GraphQLError('Поставку можно отклонить только в статусе PENDING')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'REJECTED',
supplierNotes: args.reason || 'Поставка отклонена',
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
return {
success: true,
message: 'Поставка отклонена',
order: updatedSupply,
}
} catch (error) {
console.error('Error rejecting fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка отклонения поставки',
order: null,
}
}
}
const supplierShipConsumableSupply = async (
_: unknown,
args: { id: string },
context: Context,
) => {
if (!context.user) {
throw new GraphQLError('Требуется авторизация', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
try {
const user = await prisma.user.findUnique({
where: { id: context.user.id },
include: { organization: true },
})
if (!user?.organization || user.organization.type !== 'WHOLESALE') {
throw new GraphQLError('Только поставщики могут отправлять поставки')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
supplier: true,
fulfillmentCenter: true,
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
if (supply.supplierId !== user.organizationId) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (!['SUPPLIER_APPROVED', 'LOGISTICS_CONFIRMED'].includes(supply.status)) {
throw new GraphQLError('Поставку можно отправить только в статусе SUPPLIER_APPROVED или LOGISTICS_CONFIRMED')
}
const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({
where: { id: args.id },
data: {
status: 'SHIPPED',
shippedAt: new Date(),
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
items: {
include: {
product: true,
},
},
},
})
return {
success: true,
message: 'Поставка отправлена',
order: updatedSupply,
}
} catch (error) {
console.error('Error shipping fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка отправки поставки',
order: null,
}
}
}
export const fulfillmentConsumableV2Mutations = {
createFulfillmentConsumableSupply: async (
_: unknown,
@ -267,4 +491,9 @@ export const fulfillmentConsumableV2Mutations = {
}
}
},
// Добавляем мутации поставщика
supplierApproveConsumableSupply,
supplierRejectConsumableSupply,
supplierShipConsumableSupply,
}