// ============================================================================= // 🛒 РЕЗОЛВЕРЫ ДЛЯ СИСТЕМЫ ПОСТАВОК ТОВАРОВ СЕЛЛЕРА V2 // ============================================================================= import { GraphQLError } from 'graphql' import { processSellerGoodsSupplyReceipt } from '@/lib/inventory-management-goods' import { prisma } from '@/lib/prisma' import { notifyOrganization } from '@/lib/realtime' import { Context } from '../context' // ============================================================================= // 🔍 QUERY RESOLVERS V2 // ============================================================================= export const sellerGoodsQueries = { // Мои товарные поставки (для селлеров - заказы которые я создал) mySellerGoodsSupplies: 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 !== 'SELLER') { return [] } const supplies = await prisma.sellerGoodsSupplyOrder.findMany({ where: { sellerId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, supplier: true, logisticsPartner: true, receivedBy: true, recipeItems: { include: { product: true, }, }, }, orderBy: { createdAt: 'desc', }, }) return supplies } catch (error) { console.error('Error fetching seller goods supplies:', error) return [] } }, // Входящие товарные заказы от селлеров (для фулфилмента) incomingSellerGoodsSupplies: 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') { return [] } const supplies = await prisma.sellerGoodsSupplyOrder.findMany({ where: { fulfillmentCenterId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, supplier: true, logisticsPartner: true, receivedBy: true, recipeItems: { include: { product: true, }, }, }, orderBy: { createdAt: 'desc', }, }) return supplies } catch (error) { console.error('Error fetching incoming seller goods supplies:', error) return [] } }, // Товарные заказы от селлеров (для поставщиков) mySellerGoodsSupplyRequests: 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.sellerGoodsSupplyOrder.findMany({ where: { supplierId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, supplier: true, logisticsPartner: true, receivedBy: true, recipeItems: { include: { product: true, }, }, }, orderBy: { createdAt: 'desc', }, }) return supplies } catch (error) { console.error('Error fetching seller goods supply requests:', error) return [] } }, // Получение конкретной товарной поставки селлера sellerGoodsSupply: 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.sellerGoodsSupplyOrder.findUnique({ where: { id: args.id }, include: { seller: true, fulfillmentCenter: true, supplier: true, logisticsPartner: true, receivedBy: true, recipeItems: { include: { product: true, }, }, }, }) if (!supply) { throw new GraphQLError('Поставка не найдена') } // Проверка доступа const hasAccess = (user.organization.type === 'SELLER' && supply.sellerId === user.organizationId) || (user.organization.type === 'FULFILLMENT' && supply.fulfillmentCenterId === user.organizationId) || (user.organization.type === 'WHOLESALE' && supply.supplierId === user.organizationId) || (user.organization.type === 'LOGIST' && supply.logisticsPartnerId === user.organizationId) if (!hasAccess) { throw new GraphQLError('Нет доступа к этой поставке') } return supply } catch (error) { console.error('Error fetching seller goods supply:', error) if (error instanceof GraphQLError) { throw error } throw new GraphQLError('Ошибка получения товарной поставки') } }, // Инвентарь товаров селлера на складе фулфилмента mySellerGoodsInventory: 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) { return [] } let inventoryItems if (user.organization.type === 'SELLER') { // Селлер видит свои товары на всех складах inventoryItems = await prisma.sellerGoodsInventory.findMany({ where: { sellerId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, product: true, }, orderBy: { lastSupplyDate: 'desc', }, }) } else if (user.organization.type === 'FULFILLMENT') { // Фулфилмент видит все товары на своем складе inventoryItems = await prisma.sellerGoodsInventory.findMany({ where: { fulfillmentCenterId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, product: true, }, orderBy: { lastSupplyDate: 'desc', }, }) } else { return [] } return inventoryItems } catch (error) { console.error('Error fetching seller goods inventory:', error) return [] } }, } // ============================================================================= // ✏️ MUTATION RESOLVERS V2 // ============================================================================= export const sellerGoodsMutations = { // Создание поставки товаров селлера createSellerGoodsSupply: async (_: unknown, args: { input: any }, 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 !== 'SELLER') { throw new GraphQLError('Доступно только для селлеров') } const { fulfillmentCenterId, supplierId, logisticsPartnerId, requestedDeliveryDate, notes, recipeItems } = args.input // 🔍 ВАЛИДАЦИЯ ПАРТНЕРОВ // Проверяем фулфилмент-центр const fulfillmentCenter = await prisma.organization.findUnique({ where: { id: fulfillmentCenterId }, include: { counterpartyOf: { where: { organizationId: user.organizationId! }, }, }, }) if (!fulfillmentCenter || fulfillmentCenter.type !== 'FULFILLMENT') { throw new GraphQLError('Фулфилмент-центр не найден или имеет неверный тип') } if (fulfillmentCenter.counterpartyOf.length === 0) { throw new GraphQLError('Нет партнерских отношений с данным фулфилмент-центром') } // Проверяем поставщика const supplier = await prisma.organization.findUnique({ where: { id: supplierId }, include: { counterpartyOf: { where: { organizationId: user.organizationId! }, }, }, }) if (!supplier || supplier.type !== 'WHOLESALE') { throw new GraphQLError('Поставщик не найден или имеет неверный тип') } if (supplier.counterpartyOf.length === 0) { throw new GraphQLError('Нет партнерских отношений с данным поставщиком') } // 🔍 ВАЛИДАЦИЯ ТОВАРОВ И ОСТАТКОВ let totalCost = 0 const mainProducts = recipeItems.filter((item: any) => item.recipeType === 'MAIN_PRODUCT') if (mainProducts.length === 0) { throw new GraphQLError('Должен быть хотя бы один основной товар') } // Проверяем только основные товары (MAIN_PRODUCT) в рецептуре for (const item of recipeItems) { // В V2 временно валидируем только основные товары if (item.recipeType !== 'MAIN_PRODUCT') { console.log(`⚠️ Пропускаем валидацию ${item.recipeType} товара ${item.productId} - не поддерживается в V2`) continue } const product = await prisma.product.findUnique({ where: { id: item.productId }, }) if (!product) { throw new GraphQLError(`Товар с ID ${item.productId} не найден`) } if (product.organizationId !== supplierId) { throw new GraphQLError(`Товар ${product.name} не принадлежит выбранному поставщику`) } // Проверяем остатки основных товаров const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0) if (item.quantity > availableStock) { throw new GraphQLError( `Недостаточно остатков товара "${product.name}". ` + `Доступно: ${availableStock} шт., запрашивается: ${item.quantity} шт.`, ) } totalCost += product.price.toNumber() * item.quantity } // 🚀 СОЗДАНИЕ ПОСТАВКИ В ТРАНЗАКЦИИ const supplyOrder = await prisma.$transaction(async (tx) => { // Создаем заказ поставки const newOrder = await tx.sellerGoodsSupplyOrder.create({ data: { sellerId: user.organizationId!, fulfillmentCenterId, supplierId, logisticsPartnerId, requestedDeliveryDate: new Date(requestedDeliveryDate), notes, status: 'PENDING', totalCostWithDelivery: totalCost, }, }) // Создаем записи рецептуры только для MAIN_PRODUCT for (const item of recipeItems) { // В V2 временно создаем только основные товары if (item.recipeType !== 'MAIN_PRODUCT') { console.log(`⚠️ Пропускаем создание записи для ${item.recipeType} товара ${item.productId}`) continue } await tx.goodsSupplyRecipeItem.create({ data: { supplyOrderId: newOrder.id, productId: item.productId, quantity: item.quantity, recipeType: item.recipeType, }, }) // Резервируем основные товары у поставщика await tx.product.update({ where: { id: item.productId }, data: { ordered: { increment: item.quantity, }, }, }) } return newOrder }) // 📨 УВЕДОМЛЕНИЯ await notifyOrganization( supplierId, `Новый заказ товаров от селлера ${user.organization.name}`, 'GOODS_SUPPLY_ORDER_CREATED', { orderId: supplyOrder.id }, ) await notifyOrganization( fulfillmentCenterId, `Селлер ${user.organization.name} оформил поставку товаров на ваш склад`, 'INCOMING_GOODS_SUPPLY_ORDER', { orderId: supplyOrder.id }, ) // Получаем созданную поставку с полными данными const createdSupply = await prisma.sellerGoodsSupplyOrder.findUnique({ where: { id: supplyOrder.id }, include: { seller: true, fulfillmentCenter: true, supplier: true, logisticsPartner: true, recipeItems: { include: { product: true, }, }, }, }) return { success: true, message: 'Поставка товаров успешно создана', supplyOrder: createdSupply, } } catch (error) { console.error('Error creating seller goods supply:', error) if (error instanceof GraphQLError) { throw error } throw new GraphQLError('Ошибка создания товарной поставки') } }, // Обновление статуса товарной поставки updateSellerGoodsSupplyStatus: async ( _: unknown, args: { id: string; status: string; notes?: string }, context: Context, ) => { if (!context.user) { throw new GraphQLError('Требуется авторизация') } 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.sellerGoodsSupplyOrder.findUnique({ where: { id: args.id }, include: { seller: true, supplier: true, fulfillmentCenter: true, recipeItems: { include: { product: true, }, }, }, }) if (!supply) { throw new GraphQLError('Поставка не найдена') } // 🔐 ПРОВЕРКА ПРАВ И ЛОГИКИ ПЕРЕХОДОВ СТАТУСОВ const { status } = args const currentStatus = supply.status const orgType = user.organization.type // Только поставщики могут переводить PENDING → APPROVED if (status === 'APPROVED' && currentStatus === 'PENDING') { if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) { throw new GraphQLError('Только поставщик может одобрить заказ') } } // Только поставщики могут переводить APPROVED → SHIPPED else if (status === 'SHIPPED' && currentStatus === 'APPROVED') { if (orgType !== 'WHOLESALE' || supply.supplierId !== user.organizationId) { throw new GraphQLError('Только поставщик может отметить отгрузку') } } // Только фулфилмент может переводить SHIPPED → DELIVERED else if (status === 'DELIVERED' && currentStatus === 'SHIPPED') { if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) { throw new GraphQLError('Только фулфилмент-центр может подтвердить получение') } } // Только фулфилмент может переводить DELIVERED → COMPLETED else if (status === 'COMPLETED' && currentStatus === 'DELIVERED') { if (orgType !== 'FULFILLMENT' || supply.fulfillmentCenterId !== user.organizationId) { throw new GraphQLError('Только фулфилмент-центр может завершить поставку') } } else { throw new GraphQLError('Недопустимый переход статуса') } // 📅 ОБНОВЛЕНИЕ ВРЕМЕННЫХ МЕТОК const updateData: any = { status, updatedAt: new Date(), } if (status === 'APPROVED' && orgType === 'WHOLESALE') { updateData.supplierApprovedAt = new Date() updateData.supplierNotes = args.notes } if (status === 'SHIPPED' && orgType === 'WHOLESALE') { updateData.shippedAt = new Date() } if (status === 'DELIVERED' && orgType === 'FULFILLMENT') { updateData.deliveredAt = new Date() updateData.receivedById = user.id updateData.receiptNotes = args.notes } // 🔄 ОБНОВЛЕНИЕ В БАЗЕ const updatedSupply = await prisma.sellerGoodsSupplyOrder.update({ where: { id: args.id }, data: updateData, include: { seller: true, fulfillmentCenter: true, supplier: true, logisticsPartner: true, receivedBy: true, recipeItems: { include: { product: true, }, }, }, }) // 📨 УВЕДОМЛЕНИЯ О СМЕНЕ СТАТУСА if (status === 'APPROVED') { await notifyOrganization( supply.sellerId, `Поставка товаров одобрена поставщиком ${user.organization.name}`, 'GOODS_SUPPLY_APPROVED', { orderId: args.id }, ) } if (status === 'SHIPPED') { await notifyOrganization( supply.sellerId, `Поставка товаров отгружена поставщиком ${user.organization.name}`, 'GOODS_SUPPLY_SHIPPED', { orderId: args.id }, ) await notifyOrganization( supply.fulfillmentCenterId, 'Поставка товаров в пути. Ожидается доставка', 'GOODS_SUPPLY_IN_TRANSIT', { orderId: args.id }, ) } if (status === 'DELIVERED') { // 📦 АВТОМАТИЧЕСКОЕ СОЗДАНИЕ/ОБНОВЛЕНИЕ ИНВЕНТАРЯ V2 await processSellerGoodsSupplyReceipt(args.id) await notifyOrganization( supply.sellerId, `Поставка товаров доставлена в ${supply.fulfillmentCenter.name}`, 'GOODS_SUPPLY_DELIVERED', { orderId: args.id }, ) } if (status === 'COMPLETED') { await notifyOrganization( supply.sellerId, `Поставка товаров завершена. Товары размещены на складе ${supply.fulfillmentCenter.name}`, 'GOODS_SUPPLY_COMPLETED', { orderId: args.id }, ) } return updatedSupply } catch (error) { console.error('Error updating seller goods supply status:', error) if (error instanceof GraphQLError) { throw error } throw new GraphQLError('Ошибка обновления статуса товарной поставки') } }, // Отмена товарной поставки селлером cancelSellerGoodsSupply: async (_: unknown, args: { id: string }, context: Context) => { if (!context.user) { throw new GraphQLError('Требуется авторизация') } try { const user = await prisma.user.findUnique({ where: { id: context.user.id }, include: { organization: true }, }) if (!user?.organization || user.organization.type !== 'SELLER') { throw new GraphQLError('Только селлеры могут отменять свои поставки') } const supply = await prisma.sellerGoodsSupplyOrder.findUnique({ where: { id: args.id }, include: { seller: true, recipeItems: { include: { product: true, }, }, }, }) if (!supply) { throw new GraphQLError('Поставка не найдена') } if (supply.sellerId !== user.organizationId) { throw new GraphQLError('Вы можете отменить только свои поставки') } // ✅ ПРОВЕРКА ВОЗМОЖНОСТИ ОТМЕНЫ (только PENDING и APPROVED) if (!['PENDING', 'APPROVED'].includes(supply.status)) { throw new GraphQLError('Поставку можно отменить только в статусе PENDING или APPROVED') } // 🔄 ОТМЕНА В ТРАНЗАКЦИИ const cancelledSupply = await prisma.$transaction(async (tx) => { // Обновляем статус const updated = await tx.sellerGoodsSupplyOrder.update({ where: { id: args.id }, data: { status: 'CANCELLED', updatedAt: new Date(), }, include: { seller: true, fulfillmentCenter: true, supplier: true, recipeItems: { include: { product: true, }, }, }, }) // Освобождаем зарезервированные товары у поставщика (только MAIN_PRODUCT) for (const item of supply.recipeItems) { if (item.recipeType === 'MAIN_PRODUCT') { await tx.product.update({ where: { id: item.productId }, data: { ordered: { decrement: item.quantity, }, }, }) } } return updated }) // 📨 УВЕДОМЛЕНИЯ ОБ ОТМЕНЕ if (supply.supplierId) { await notifyOrganization( supply.supplierId, `Селлер ${user.organization.name} отменил заказ товаров`, 'GOODS_SUPPLY_CANCELLED', { orderId: args.id }, ) } await notifyOrganization( supply.fulfillmentCenterId, `Селлер ${user.organization.name} отменил поставку товаров`, 'GOODS_SUPPLY_CANCELLED', { orderId: args.id }, ) return cancelledSupply } catch (error) { console.error('Error cancelling seller goods supply:', error) if (error instanceof GraphQLError) { throw error } throw new GraphQLError('Ошибка отмены товарной поставки') } }, }