Files
sfera-new/src/graphql/resolvers/fulfillment-consumables-v2.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

499 lines
15 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.

import { GraphQLError } from 'graphql'
import { prisma } from '@/lib/prisma'
import { notifyOrganization } from '@/lib/realtime'
import { Context } from '../context'
export const fulfillmentConsumableV2Queries = {
myFulfillmentConsumableSupplies: async (_: unknown, __: unknown, 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 !== 'FULFILLMENT') {
throw new GraphQLError('Доступно только для фулфилмент-центров')
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
fulfillmentCenterId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching fulfillment consumable supplies:', error)
return [] // Возвращаем пустой массив вместо throw
}
},
fulfillmentConsumableSupply: 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) {
throw new GraphQLError('Организация не найдена')
}
const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({
where: { id: args.id },
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
})
if (!supply) {
throw new GraphQLError('Поставка не найдена')
}
// Проверка доступа
if (
user.organization.type === 'FULFILLMENT' &&
supply.fulfillmentCenterId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
if (
user.organization.type === 'WHOLESALE' &&
supply.supplierId !== user.organizationId
) {
throw new GraphQLError('Нет доступа к этой поставке')
}
return supply
} catch (error) {
console.error('Error fetching fulfillment consumable supply:', error)
throw new GraphQLError('Ошибка получения поставки')
}
},
// Заявки на поставки для поставщиков (новая система v2)
mySupplierConsumableSupplies: async (_: unknown, __: unknown, 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') {
return []
}
const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({
where: {
supplierId: user.organizationId!,
},
include: {
fulfillmentCenter: true,
supplier: true,
logisticsPartner: true,
receivedBy: true,
items: {
include: {
product: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return supplies
} catch (error) {
console.error('Error fetching supplier consumable supplies:', error)
return []
}
},
}
// =============================================================================
// 🔄 МУТАЦИИ ПОСТАВЩИКА ДЛЯ 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,
args: {
input: {
supplierId: string
requestedDeliveryDate: string
items: Array<{
productId: string
requestedQuantity: number
}>
notes?: 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 !== 'FULFILLMENT') {
throw new GraphQLError('Только фулфилмент-центры могут создавать поставки расходников')
}
// Проверяем что поставщик существует и является WHOLESALE
const supplier = await prisma.organization.findUnique({
where: { id: args.input.supplierId },
})
if (!supplier || supplier.type !== 'WHOLESALE') {
throw new GraphQLError('Поставщик не найден или не является оптовиком')
}
// Проверяем что все товары существуют и принадлежат поставщику
const productIds = args.input.items.map(item => item.productId)
const products = await prisma.product.findMany({
where: {
id: { in: productIds },
organizationId: supplier.id,
type: 'CONSUMABLE',
},
})
if (products.length !== productIds.length) {
throw new GraphQLError('Некоторые товары не найдены или не принадлежат поставщику')
}
// Создаем поставку с items
const supplyOrder = await prisma.fulfillmentConsumableSupplyOrder.create({
data: {
fulfillmentCenterId: user.organizationId!,
supplierId: supplier.id,
requestedDeliveryDate: new Date(args.input.requestedDeliveryDate),
notes: args.input.notes,
items: {
create: args.input.items.map(item => {
const product = products.find(p => p.id === item.productId)!
return {
productId: item.productId,
requestedQuantity: item.requestedQuantity,
unitPrice: product.price,
totalPrice: product.price.mul(item.requestedQuantity),
}
}),
},
},
include: {
fulfillmentCenter: true,
supplier: true,
items: {
include: {
product: true,
},
},
},
})
// Отправляем уведомление поставщику о новой заявке
await notifyOrganization(supplier.id, {
type: 'supply-order:new',
title: 'Новая заявка на поставку расходников',
message: `Фулфилмент-центр "${user.organization.name}" создал заявку на поставку расходников`,
data: {
supplyOrderId: supplyOrder.id,
supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2',
fulfillmentCenterName: user.organization.name,
itemsCount: args.input.items.length,
requestedDeliveryDate: args.input.requestedDeliveryDate,
},
})
return {
success: true,
message: 'Поставка расходников создана успешно',
supplyOrder,
}
} catch (error) {
console.error('Error creating fulfillment consumable supply:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Ошибка создания поставки',
supplyOrder: null,
}
}
},
// Добавляем мутации поставщика
supplierApproveConsumableSupply,
supplierRejectConsumableSupply,
supplierShipConsumableSupply,
}