import { GraphQLError } from 'graphql' import { Context } from '../../context' import { prisma } from '../../../lib/prisma' import { notifyOrganization } from '../../../lib/realtime' import { processSupplyOrderReceipt } from '../../../lib/inventory-management' import { DomainResolvers } from '../shared/types' import { getCurrentUser, withAuth, withOrgTypeAuth } from '../shared/auth-utils' // ============================================================================= // 🔐 ЛОКАЛЬНЫЕ AUTH HELPERS // ============================================================================= const checkFulfillmentAccess = async (userId: string) => { const user = await prisma.user.findUnique({ where: { id: userId }, include: { organization: true }, }) if (!user?.organizationId) { throw new GraphQLError('Пользователь не привязан к организации', { extensions: { code: 'FORBIDDEN' }, }) } if (!user.organization || user.organization.type !== 'FULFILLMENT') { throw new GraphQLError('Доступно только для фулфилмент-центров', { extensions: { code: 'FORBIDDEN' }, }) } return user } // ============================================================================= // 📦 INVENTORY DOMAIN RESOLVERS // ============================================================================= export const inventoryResolvers: DomainResolvers = { Query: { // Мои поставки расходников (для фулфилмента) - ОПТИМИЗИРОВАНО myFulfillmentConsumableSupplies: withOrgTypeAuth(['FULFILLMENT'], async (_: unknown, __: unknown, context: Context, user: any) => { console.log('🔍 MY_FULFILLMENT_CONSUMABLE_SUPPLIES DOMAIN QUERY STARTED:', { userId: context.user?.id, organizationId: user.organizationId }) const supplies = await prisma.fulfillmentConsumableSupplyOrder.findMany({ where: { fulfillmentCenterId: user.organizationId, }, select: { id: true, status: true, requestedDeliveryDate: true, totalItems: true, createdAt: true, updatedAt: true, // Оптимизированные select для связанных объектов fulfillmentCenter: { select: { id: true, name: true, type: true } }, supplier: { select: { id: true, name: true, type: true } }, logisticsPartner: { select: { id: true, name: true, type: true } }, receivedBy: { select: { id: true, managerName: true } }, items: { select: { id: true, quantity: true, receivedQuantity: true, product: { select: { id: true, name: true, article: true, type: true } } } }, }, orderBy: { createdAt: 'desc', }, take: 100, // Добавляем пагинацию для производительности }) console.log('✅ MY_FULFILLMENT_CONSUMABLE_SUPPLIES DOMAIN SUCCESS:', { count: supplies.length }) return supplies } ), // Детальная информация о поставке расходников - ОПТИМИЗИРОВАНО fulfillmentConsumableSupply: withAuth(async (_: unknown, args: { id: string }, context: Context) => { console.log('🔍 FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN QUERY STARTED:', { userId: context.user?.id, supplyId: args.id }) // Используем кешированного пользователя const user = await getCurrentUser(context) const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({ where: { id: args.id }, select: { id: true, status: true, requestedDeliveryDate: true, totalItems: true, fulfillmentCenterId: true, supplierId: true, createdAt: true, updatedAt: true, // Оптимизированные select fulfillmentCenter: { select: { id: true, name: true, type: true } }, supplier: { select: { id: true, name: true, type: true } }, logisticsPartner: { select: { id: true, name: true, type: true } }, receivedBy: { select: { id: true, managerName: true } }, items: { select: { id: true, quantity: true, receivedQuantity: true, product: { select: { id: true, name: true, article: true, type: 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('Нет доступа к этой поставке') } console.log('✅ FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: supply.id }) return supply }), // Складские остатки фулфилмента (V2 система инвентаря) myFulfillmentSupplies: withAuth(async (_: unknown, __: unknown, context: Context) => { console.log('🔍 MY_FULFILLMENT_SUPPLIES DOMAIN QUERY STARTED:', { userId: context.user?.id }) try { const user = await checkFulfillmentAccess(context.user!.id) // Получаем складские остатки из новой V2 модели const inventory = await prisma.fulfillmentConsumableInventory.findMany({ where: { fulfillmentCenterId: user.organizationId!, }, include: { fulfillmentCenter: true, product: { include: { organization: true, // Поставщик товара }, }, }, orderBy: { updatedAt: 'desc', }, }) // Преобразуем V2 данные в формат Supply для совместимости с фронтендом const suppliesFormatted = inventory.map((item) => { const status = item.currentStock > 0 ? 'На складе' : 'Недоступен' const supplier = item.product.organization?.name || 'Неизвестен' return { // ИДЕНТИФИКАЦИЯ id: item.id, productId: item.product.id, // ОСНОВНЫЕ ДАННЫЕ name: item.product.name, article: item.product.article, description: item.product.description || '', unit: item.product.unit || 'шт', category: item.product.category || 'Расходники', imageUrl: item.product.imageUrl, // ЦЕНЫ price: parseFloat(item.averageCost.toString()), pricePerUnit: item.resalePrice ? parseFloat(item.resalePrice.toString()) : null, // СКЛАДСКИЕ ДАННЫЕ currentStock: item.currentStock, minStock: item.minStock, usedStock: item.totalShipped || 0, quantity: item.totalReceived, warehouseStock: item.currentStock, reservedStock: item.reservedStock, // ОТГРУЗКИ shippedQuantity: item.totalShipped, totalShipped: item.totalShipped, // СТАТУС И МЕТАДАННЫЕ status, isAvailable: item.currentStock > 0, supplier, date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(), createdAt: item.createdAt.toISOString(), updatedAt: item.updatedAt.toISOString(), // ДОПОЛНИТЕЛЬНЫЕ ПОЛЯ notes: item.notes, warehouseConsumableId: item.id, actualQuantity: item.currentStock, } }) console.log('✅ MY_FULFILLMENT_SUPPLIES DOMAIN SUCCESS:', { count: suppliesFormatted.length, totalStock: suppliesFormatted.reduce((sum, item) => sum + item.currentStock, 0) }) return suppliesFormatted } catch (error) { console.error('❌ MY_FULFILLMENT_SUPPLIES DOMAIN ERROR:', error) return [] } }), // Расходники селлера на складе фулфилмента (для селлера) mySellerConsumableInventory: withAuth(async (_: unknown, __: unknown, context: Context) => { console.log('🔍 MY_SELLER_CONSUMABLE_INVENTORY DOMAIN QUERY STARTED:', { userId: context.user?.id }) 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('Доступно только для селлеров') } // Получаем складские остатки расходников селлера из V2 модели const inventory = await prisma.sellerConsumableInventory.findMany({ where: { sellerId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, product: { include: { organization: true, }, }, }, orderBy: { updatedAt: 'desc', }, }) // Преобразуем V2 данные в формат Supply для совместимости с фронтендом const suppliesFormatted = inventory.map((item) => { const status = item.currentStock > 0 ? 'На складе' : 'Недоступен' const supplier = item.product.organization?.name || 'Неизвестен' return { // ИДЕНТИФИКАЦИЯ id: item.id, productId: item.product.id, // ОСНОВНЫЕ ДАННЫЕ name: item.product.name, article: item.product.article, description: item.product.description || '', unit: item.product.unit || 'шт', category: item.product.category || 'Расходники', imageUrl: item.product.imageUrl, // ЦЕНЫ price: parseFloat(item.averageCost.toString()), pricePerUnit: item.usagePrice ? parseFloat(item.usagePrice.toString()) : null, // СКЛАДСКИЕ ДАННЫЕ currentStock: item.currentStock, minStock: item.minStock, usedStock: item.totalUsed || 0, quantity: item.totalReceived, warehouseStock: item.currentStock, reservedStock: item.reservedStock, // ИСПОЛЬЗОВАНИЕ shippedQuantity: item.totalUsed, totalShipped: item.totalUsed, // СТАТУС И МЕТАДАННЫЕ status, isAvailable: item.currentStock > 0, supplier, date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(), createdAt: item.createdAt.toISOString(), updatedAt: item.updatedAt.toISOString(), // ДОПОЛНИТЕЛЬНЫЕ ПОЛЯ notes: item.notes, warehouseConsumableId: item.id, fulfillmentCenter: item.fulfillmentCenter.name, actualQuantity: item.currentStock, } }) console.log('✅ MY_SELLER_CONSUMABLE_INVENTORY DOMAIN SUCCESS:', { count: suppliesFormatted.length, totalStock: suppliesFormatted.reduce((sum, item) => sum + item.currentStock, 0) }) return suppliesFormatted } catch (error) { console.error('❌ MY_SELLER_CONSUMABLE_INVENTORY DOMAIN ERROR:', error) return [] } }), // Расходники всех селлеров на складе фулфилмента (для фулфилмента) allSellerConsumableInventory: withAuth(async (_: unknown, __: unknown, context: Context) => { console.log('🔍 ALL_SELLER_CONSUMABLE_INVENTORY DOMAIN QUERY STARTED:', { userId: context.user?.id }) try { const user = await checkFulfillmentAccess(context.user!.id) // Получаем складские остатки всех селлеров на нашем складе const inventory = await prisma.sellerConsumableInventory.findMany({ where: { fulfillmentCenterId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, product: { include: { organization: true, }, }, }, orderBy: [ { seller: { name: 'asc' } }, { updatedAt: 'desc' }, ], }) // Возвращаем данные сгруппированные по селлерам const result = inventory.map((item) => { const status = item.currentStock > 0 ? 'На складе' : 'Недоступен' const supplier = item.product.organization?.name || 'Неизвестен' return { // ИДЕНТИФИКАЦИЯ id: item.id, productId: item.product.id, sellerId: item.sellerId, sellerName: item.seller.name, // ОСНОВНЫЕ ДАННЫЕ name: item.product.name, article: item.product.article, description: item.product.description || '', unit: item.product.unit || 'шт', category: item.product.category || 'Расходники', imageUrl: item.product.imageUrl, // СКЛАДСКИЕ ДАННЫЕ currentStock: item.currentStock, minStock: item.minStock, usedStock: item.totalUsed || 0, quantity: item.totalReceived, reservedStock: item.reservedStock, // ЦЕНЫ price: parseFloat(item.averageCost.toString()), pricePerUnit: item.usagePrice ? parseFloat(item.usagePrice.toString()) : null, // МЕТАДАННЫЕ status, isAvailable: item.currentStock > 0, supplier, date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(), createdAt: item.createdAt.toISOString(), updatedAt: item.updatedAt.toISOString(), notes: item.notes, warehouseConsumableId: item.id, actualQuantity: item.currentStock, } }) console.log('✅ ALL_SELLER_CONSUMABLE_INVENTORY DOMAIN SUCCESS:', { count: result.length, uniqueSellers: new Set(inventory.map(item => item.sellerId)).size }) return result } catch (error) { console.error('❌ ALL_SELLER_CONSUMABLE_INVENTORY DOMAIN ERROR:', error) return [] } }), // Заявки на поставки для поставщиков (новая система v2) mySupplierConsumableSupplies: withAuth(async (_: unknown, __: unknown, context: Context) => { console.log('🔍 MY_SUPPLIER_CONSUMABLE_SUPPLIES DOMAIN QUERY STARTED:', { userId: context.user?.id }) try { const user = await prisma.user.findUnique({ where: { id: context.user!.id }, include: { organization: true }, }) if (!user?.organization || user.organization.type !== 'WHOLESALE') { console.log('⚠️ User is not wholesale, returning empty array') 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', }, }) console.log('✅ MY_SUPPLIER_CONSUMABLE_SUPPLIES DOMAIN SUCCESS:', { count: supplies.length }) return supplies } catch (error) { console.error('❌ MY_SUPPLIER_CONSUMABLE_SUPPLIES DOMAIN ERROR:', error) return [] } }), // Расходники селлеров на складе фулфилмента (V2 система) sellerSuppliesOnWarehouse: withAuth(async (_: unknown, __: unknown, context: Context) => { console.log('🔍 SELLER_SUPPLIES_ON_WAREHOUSE DOMAIN QUERY STARTED:', { userId: context.user?.id }) try { const user = await checkFulfillmentAccess(context.user!.id) // V2: Получаем данные из SellerConsumableInventory const sellerInventory = await prisma.sellerConsumableInventory.findMany({ where: { fulfillmentCenterId: user.organizationId!, }, include: { seller: true, fulfillmentCenter: true, product: { include: { organization: true, // Поставщик товара }, }, }, orderBy: [ { seller: { name: 'asc' } }, // Группируем по селлерам { updatedAt: 'desc' }, ], }) console.log('📊 V2 Seller Inventory loaded for warehouse:', { fulfillmentId: user.organizationId, fulfillmentName: user.organization?.name, inventoryCount: sellerInventory.length, uniqueSellers: new Set(sellerInventory.map(item => item.sellerId)).size, }) // Преобразуем V2 данные в формат Supply для совместимости с фронтендом const suppliesFormatted = sellerInventory.map((item) => { const status = item.currentStock > 0 ? 'На складе' : 'Недоступен' return { // ИДЕНТИФИКАЦИЯ id: item.id, productId: item.product.id, sellerId: item.seller.id, // ОСНОВНЫЕ ДАННЫЕ name: item.product.name, article: item.product.article, description: item.product.description || '', unit: item.product.unit || 'шт', imageUrl: item.product.imageUrl, // ЦЕНЫ price: parseFloat(item.averageCost.toString()), // СКЛАДСКИЕ ДАННЫЕ currentStock: item.currentStock, quantity: item.totalReceived, usedQuantity: item.totalShipped || 0, // СТАТУС И МЕТАДАННЫЕ status, isAvailable: item.currentStock > 0, sellerName: item.seller.name, supplierName: item.product.organization?.name || 'Неизвестен', date: item.lastSupplyDate?.toISOString() || item.createdAt.toISOString(), createdAt: item.createdAt.toISOString(), updatedAt: item.updatedAt.toISOString(), } }) console.log('✅ SELLER_SUPPLIES_ON_WAREHOUSE DOMAIN SUCCESS:', { suppliesCount: suppliesFormatted.length, sellersCount: new Set(sellerInventory.map(item => item.sellerId)).size, }) return suppliesFormatted } catch (error) { console.error('❌ SELLER_SUPPLIES_ON_WAREHOUSE DOMAIN ERROR:', error) return [] } }), // Данные склада с партнерами (3-уровневая иерархия) warehouseData: withAuth(async (_: unknown, __: unknown, context: Context) => { console.log('🔍 WAREHOUSE_DATA DOMAIN QUERY STARTED:', { userId: context.user?.id }) try { const user = await checkFulfillmentAccess(context.user!.id) // Получаем партнеров (селлеров) этого фулфилмента const partnerships = await prisma.counterparty.findMany({ where: { fulfillmentId: user.organizationId!, status: 'APPROVED', }, include: { seller: { include: { users: true, }, }, }, orderBy: { createdAt: 'desc' }, }) // Получаем данные склада по каждому партнеру const warehouseData = await Promise.all( partnerships.map(async (partnership) => { // Получаем расходники этого селлера на складе const inventory = await prisma.sellerConsumableInventory.findMany({ where: { sellerId: partnership.sellerId, fulfillmentCenterId: user.organizationId!, }, include: { product: { include: { organization: true, }, }, }, }) // Подсчитываем статистику const totalItems = inventory.reduce((sum, item) => sum + item.currentStock, 0) const totalValue = inventory.reduce((sum, item) => sum + (item.currentStock * parseFloat(item.averageCost.toString())), 0) const uniqueProducts = inventory.length return { // ПАРТНЕР (СЕЛЛЕР) partnerId: partnership.seller.id, partnerName: partnership.seller.name, partnerInn: partnership.seller.inn, partnerType: partnership.seller.type, // СТАТИСТИКА СКЛАДА totalItems, totalValue, uniqueProducts, // ДЕТАЛИ ИНВЕНТАРЯ inventory: inventory.map(item => ({ id: item.id, productId: item.product.id, name: item.product.name, article: item.product.article, currentStock: item.currentStock, averageCost: parseFloat(item.averageCost.toString()), supplierName: item.product.organization?.name || 'Неизвестен', lastSupplyDate: item.lastSupplyDate, })), // МЕТАДАННЫЕ partnershipDate: partnership.createdAt, lastUpdate: partnership.updatedAt, } }) ) console.log('✅ WAREHOUSE_DATA DOMAIN SUCCESS:', { partnersCount: warehouseData.length, totalInventoryItems: warehouseData.reduce((sum, p) => sum + p.totalItems, 0), }) return warehouseData } catch (error) { console.error('❌ WAREHOUSE_DATA DOMAIN ERROR:', error) return [] } }), }, Mutation: { // Создание поставки расходников (фулфилмент → поставщик) createFulfillmentConsumableSupply: withAuth(async ( _: unknown, args: { input: { supplierId: string requestedDeliveryDate: string items: Array<{ productId: string requestedQuantity: number }> notes?: string } }, context: Context, ) => { console.log('🔍 CREATE_FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', { userId: context.user?.id, supplierId: args.input.supplierId }) try { const user = await checkFulfillmentAccess(context.user!.id) // Проверяем что поставщик существует и является 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, }, }) const result = { success: true, message: 'Поставка расходников создана успешно', supplyOrder, } console.log('✅ CREATE_FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyOrderId: supplyOrder.id }) return result } catch (error: any) { console.error('❌ CREATE_FULFILLMENT_CONSUMABLE_SUPPLY DOMAIN ERROR:', error) return { success: false, message: error.message || 'Ошибка создания поставки', supplyOrder: null, } } }), // Поставщик одобряет поставку расходников supplierApproveConsumableSupply: withAuth(async ( _: unknown, args: { id: string }, context: Context, ) => { console.log('🔍 SUPPLIER_APPROVE_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', { userId: context.user?.id, supplyId: args.id }) try { const user = await checkWholesaleAccess(context.user!.id) 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, }, }, }, }) const result = { success: true, message: 'Поставка одобрена успешно', order: updatedSupply, } console.log('✅ SUPPLIER_APPROVE_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id }) return result } catch (error: any) { console.error('❌ SUPPLIER_APPROVE_CONSUMABLE_SUPPLY DOMAIN ERROR:', error) return { success: false, message: error.message || 'Ошибка одобрения поставки', order: null, } } }), // Поставщик отклоняет поставку расходников supplierRejectConsumableSupply: withAuth(async ( _: unknown, args: { id: string; reason?: string }, context: Context, ) => { console.log('🔍 SUPPLIER_REJECT_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', { userId: context.user?.id, supplyId: args.id, reason: args.reason }) try { const user = await checkWholesaleAccess(context.user!.id) 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, }, }, }, }) const result = { success: true, message: 'Поставка отклонена', order: updatedSupply, } console.log('✅ SUPPLIER_REJECT_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id }) return result } catch (error: any) { console.error('❌ SUPPLIER_REJECT_CONSUMABLE_SUPPLY DOMAIN ERROR:', error) return { success: false, message: error.message || 'Ошибка отклонения поставки', order: null, } } }), // Поставщик отправляет поставку расходников supplierShipConsumableSupply: withAuth(async ( _: unknown, args: { id: string }, context: Context, ) => { console.log('🔍 SUPPLIER_SHIP_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', { userId: context.user?.id, supplyId: args.id }) try { const user = await checkWholesaleAccess(context.user!.id) 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, }, }, }, }) const result = { success: true, message: 'Поставка отправлена', order: updatedSupply, } console.log('✅ SUPPLIER_SHIP_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id }) return result } catch (error: any) { console.error('❌ SUPPLIER_SHIP_CONSUMABLE_SUPPLY DOMAIN ERROR:', error) return { success: false, message: error.message || 'Ошибка отправки поставки', order: null, } } }), // Фулфилмент принимает поставку расходников fulfillmentReceiveConsumableSupply: withAuth(async ( _: unknown, args: { id: string items: Array<{ id: string; receivedQuantity: number; defectQuantity?: number }> notes?: string }, context: Context, ) => { console.log('🔍 FULFILLMENT_RECEIVE_CONSUMABLE_SUPPLY DOMAIN MUTATION STARTED:', { userId: context.user?.id, supplyId: args.id, itemsCount: args.items.length }) try { const user = await checkFulfillmentAccess(context.user!.id) const supply = await prisma.fulfillmentConsumableSupplyOrder.findUnique({ where: { id: args.id }, include: { fulfillmentCenter: true, supplier: true, items: { include: { product: true, }, }, }, }) if (!supply) { throw new GraphQLError('Поставка не найдена') } if (supply.fulfillmentCenterId !== user.organizationId) { throw new GraphQLError('Нет доступа к этой поставке') } if (supply.status !== 'SHIPPED') { throw new GraphQLError('Поставку можно принять только в статусе SHIPPED') } // Обновляем статус поставки на DELIVERED const updatedSupply = await prisma.fulfillmentConsumableSupplyOrder.update({ where: { id: args.id }, data: { status: 'DELIVERED', receivedAt: new Date(), receivedById: user.id, receiptNotes: args.notes, }, include: { fulfillmentCenter: true, supplier: true, items: { include: { product: true, }, }, }, }) // Обновляем фактические количества товаров for (const itemData of args.items) { await prisma.fulfillmentConsumableSupplyItem.updateMany({ where: { id: itemData.id }, data: { receivedQuantity: itemData.receivedQuantity, defectQuantity: itemData.defectQuantity || 0, }, }) } // Обновляем складские остатки в FulfillmentConsumableInventory const inventoryItems = args.items.map(item => { const supplyItem = supply.items.find(si => si.id === item.id) if (!supplyItem) { throw new GraphQLError(`Товар поставки не найден: ${item.id}`) } return { productId: supplyItem.productId, receivedQuantity: item.receivedQuantity, unitPrice: parseFloat(supplyItem.unitPrice.toString()), } }) await processSupplyOrderReceipt(supply.id, inventoryItems) console.log('✅ FULFILLMENT_RECEIVE_SUPPLY: Inventory updated:', { supplyId: supply.id, itemsCount: inventoryItems.length, totalReceived: inventoryItems.reduce((sum, item) => sum + item.receivedQuantity, 0), }) // Уведомляем поставщика о приемке if (supply.supplierId) { await notifyOrganization(supply.supplierId, { type: 'supply-order:delivered', title: 'Поставка принята фулфилментом', message: `Фулфилмент-центр "${supply.fulfillmentCenter.name}" принял поставку`, data: { supplyOrderId: supply.id, supplyOrderType: 'FULFILLMENT_CONSUMABLES_V2', fulfillmentCenterName: supply.fulfillmentCenter.name, itemsCount: inventoryItems.length, totalReceived: inventoryItems.reduce((sum, item) => sum + item.receivedQuantity, 0), }, }) } const result = { success: true, message: 'Поставка успешно принята', order: updatedSupply, } console.log('✅ FULFILLMENT_RECEIVE_CONSUMABLE_SUPPLY DOMAIN SUCCESS:', { supplyId: updatedSupply.id }) return result } catch (error: any) { console.error('❌ FULFILLMENT_RECEIVE_CONSUMABLE_SUPPLY DOMAIN ERROR:', error) return { success: false, message: error.message || 'Ошибка приемки поставки', order: null, } } }), // V1 Legacy: Резервирование товара на складе reserveProductStock: async ( _: unknown, args: { productId: string quantity: number reason?: string }, context: Context, ) => { console.warn('🔒 RESERVE_PRODUCT_STOCK (V1) - LEGACY RESOLVER:', { productId: args.productId, quantity: args.quantity, reason: args.reason, }) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // TODO: Реализовать V1 логику резервирования товара на складе // Может потребоваться миграция на V2 систему управления запасами return { success: false, message: 'V1 Legacy - требуется реализация', reservationId: null, } }, // V1 Legacy: Освобождение резерва товара releaseProductReserve: async ( _: unknown, args: { reservationId: string reason?: string }, context: Context, ) => { console.warn('🔓 RELEASE_PRODUCT_RESERVE (V1) - LEGACY RESOLVER:', { reservationId: args.reservationId, reason: args.reason, }) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // TODO: Реализовать V1 логику освобождения резерва товара // Может потребоваться миграция на V2 систему управления резервированием return { success: false, message: 'V1 Legacy - требуется реализация', } }, // V1 Legacy: Обновить статус товара в пути updateProductInTransit: async ( _: unknown, args: { productId: string transitData: { status: string location?: string estimatedArrival?: string trackingNumber?: string } }, context: Context, ) => { console.warn('🚛 UPDATE_PRODUCT_IN_TRANSIT (V1) - LEGACY RESOLVER:', { productId: args.productId, transitStatus: args.transitData.status, location: args.transitData.location, }) if (!context.user) { throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } // TODO: Реализовать V1 логику отслеживания товаров в пути // Может потребоваться интеграция с V2 системой логистики return { success: false, message: 'V1 Legacy - требуется реализация', product: null, } }, }, } console.warn('🔥 INVENTORY DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ')