import { GraphQLError } from 'graphql' import { prisma } from '../../../lib/prisma' import { Context } from '../../context' import { DomainResolvers } from '../shared/types' // Analytics Domain Resolvers - управление аналитикой и статистикой // ============================================================================= // 🔐 AUTHENTICATION HELPERS // ============================================================================= const withAuth = (resolver: any) => { return async (parent: any, args: any, context: Context) => { console.log('🔐 ANALYTICS DOMAIN AUTH CHECK:', { hasUser: !!context.user, userId: context.user?.id, organizationId: context.user?.organizationId, }) if (!context.user) { console.error('❌ AUTH FAILED: No user in context') throw new GraphQLError('Требуется авторизация', { extensions: { code: 'UNAUTHENTICATED' }, }) } console.log('✅ AUTH PASSED: Calling resolver') try { const result = await resolver(parent, args, context) console.log('🎯 RESOLVER RESULT TYPE:', typeof result, result === null ? 'NULL RESULT!' : 'Has result') return result } catch (error) { console.error('💥 RESOLVER ERROR:', error) throw error } } } // ============================================================================= // 📊 ANALYTICS DOMAIN RESOLVERS // ============================================================================= export const analyticsResolvers: DomainResolvers = { Query: { // Статистика склада фулфилмента с изменениями за сутки (V2 - только модульные модели) fulfillmentWarehouseStats: withAuth(async (_: unknown, __: unknown, context: Context) => { console.log('🔍 FULFILLMENT_WAREHOUSE_STATS V2 DOMAIN QUERY STARTED:', { userId: context.user?.id }) try { const currentUser = await prisma.user.findUnique({ where: { id: context.user!.id }, include: { organization: true }, }) if (!currentUser?.organization) { throw new GraphQLError('У пользователя нет организации') } if (currentUser.organization.type !== 'FULFILLMENT') { throw new GraphQLError('Доступ разрешен только для фулфилмент-центров') } const organizationId = currentUser.organization.id // Временные периоды для анализа изменений const yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) yesterday.setHours(0, 0, 0, 0) const today = new Date() today.setHours(0, 0, 0, 0) console.log('📊 Calculating V2 warehouse stats:', { organizationId, yesterday: yesterday.toISOString(), today: today.toISOString(), }) // ============================================================================= // 📦 V2 МОДЕЛЬ: FulfillmentConsumableInventory - расходники на складе ФФ // ============================================================================= const [consumableInventory, consumableLowStock, yesterdayConsumableStock] = await Promise.all([ // Текущий остаток расходников prisma.fulfillmentConsumableInventory.aggregate({ where: { fulfillmentCenterId: organizationId }, _sum: { currentStock: true }, _count: true, }), // Расходники с низким остатком prisma.fulfillmentConsumableInventory.count({ where: { fulfillmentCenterId: organizationId, OR: [ { currentStock: { lte: 10 } }, // Критический остаток // TODO: Добавить поле minStock в схему для точного расчета ], }, }), // Вчерашний остаток расходников (для расчета изменений) prisma.fulfillmentConsumableInventory.aggregate({ where: { fulfillmentCenterId: organizationId, updatedAt: { gte: yesterday, lt: today }, }, _sum: { currentStock: true }, }), ]) // ============================================================================= // 📋 V2 МОДЕЛЬ: FulfillmentConsumableSupplyOrder - заказы расходников // ============================================================================= const [recentSupplyOrders, pendingSupplyOrders, todaySupplyOrders] = await Promise.all([ // Недавние заказы расходников (за сутки) prisma.fulfillmentConsumableSupplyOrder.count({ where: { fulfillmentCenterId: organizationId, createdAt: { gte: yesterday }, }, }), // Ожидающие заказы расходников prisma.fulfillmentConsumableSupplyOrder.count({ where: { fulfillmentCenterId: organizationId, status: 'PENDING', }, }), // Заказы расходников за сегодня prisma.fulfillmentConsumableSupplyOrder.count({ where: { fulfillmentCenterId: organizationId, createdAt: { gte: today }, }, }), ]) // ============================================================================= // 📦 V2 МОДЕЛЬ: SellerGoodsInventory - товары селлеров на складе ФФ // ============================================================================= const [sellerGoodsInventory] = await Promise.all([ // Товары селлеров на складе ФФ prisma.sellerGoodsInventory.aggregate({ where: { fulfillmentCenterId: organizationId, // товары хранятся у этого ФФ }, _sum: { currentStock: true }, _count: true, }), ]) // ============================================================================= // 🧮 РАСЧЕТ ИЗМЕНЕНИЙ ЗА СУТКИ // ============================================================================= const consumableStockChange = (consumableInventory._sum.currentStock || 0) - (yesterdayConsumableStock._sum.currentStock || 0) const supplyOrdersChange = todaySupplyOrders // ============================================================================= // 📊 ФОРМИРОВАНИЕ СТАТИСТИКИ ПО СПЕЦИФИКАЦИИ GraphQL // ============================================================================= const warehouseStats = { // Товары (Products) - расходники на складе ФФ products: { current: consumableInventory._sum.currentStock || 0, change: consumableStockChange, percentChange: (yesterdayConsumableStock._sum.currentStock || 0) > 0 ? (consumableStockChange / (yesterdayConsumableStock._sum.currentStock || 1)) * 100 : 0, }, // Товары селлеров (Goods) - готовая продукция на складе ФФ goods: { current: sellerGoodsInventory._sum.currentStock || 0, change: 0, // TODO: добавить расчет изменений товаров за сутки percentChange: 0, }, // Дефекты (пока заглушка - в V2 системе пока нет отдельной модели дефектов) defects: { current: 0, change: 0, percentChange: 0, }, // Возвраты с ПВЗ (пока заглушка - в V2 системе пока нет модели возвратов) pvzReturns: { current: 0, change: 0, percentChange: 0, }, // Поставки расходников фулфилменту fulfillmentSupplies: { current: recentSupplyOrders, change: supplyOrdersChange, percentChange: recentSupplyOrders > 0 ? (supplyOrdersChange / recentSupplyOrders) * 100 : 0, }, // Расходники селлеров (низкий остаток = требует внимания) sellerSupplies: { current: consumableLowStock, change: 0, // TODO: добавить расчет изменения критических остатков percentChange: 0, }, } console.log('✅ FULFILLMENT_WAREHOUSE_STATS V2 DOMAIN SUCCESS:', { consumableStock: consumableInventory._sum.currentStock, sellerGoodsStock: sellerGoodsInventory._sum.currentStock, pendingOrders: pendingSupplyOrders, lowStockItems: consumableLowStock, migration: 'V2_MODELS_ONLY', }) return warehouseStats } catch (error) { console.error('❌ FULFILLMENT_WAREHOUSE_STATS V2 DOMAIN ERROR:', error) throw error } }), // УДАЛЕН: myReferralStats перемещен в referrals.ts для устранения конфликта resolver'ов // УДАЛЕН: getAvailableSuppliesForRecipe - неиспользуемый стаб резолвер V1 // УДАЛЕН: supplyMovements - неиспользуемый стаб резолвер V1 // Получение кеша статистики селлера getSellerStatsCache: withAuth(async ( _: unknown, args: { period: string; dateFrom?: string | null; dateTo?: string | null }, context: Context, ) => { console.log('🔍 GET_SELLER_STATS_CACHE DOMAIN QUERY STARTED:', { userId: context.user?.id, period: args.period, dateFrom: args.dateFrom, dateTo: args.dateTo, }) try { const user = await prisma.user.findUnique({ where: { id: context.user!.id }, include: { organization: true }, }) if (!user?.organization) { throw new GraphQLError('Пользователь не привязан к организации') } // TODO: Реализовать получение кеша из отдельной таблицы или Redis // Пока возвращаем базовую структуру const cacheKey = `seller_stats_${user.organization.id}_${args.period}_${args.dateFrom || 'null'}_${args.dateTo || 'null'}` console.log('🔍 Looking for cache:', { cacheKey }) // Заглушка для кеша - в реальной системе здесь будет запрос к кеш-хранилищу const mockCacheData = { period: args.period, dateFrom: args.dateFrom, dateTo: args.dateTo, productsData: null, productsTotalSales: 0, totalRevenue: 0, totalOrders: 0, averageOrderValue: 0, topProducts: [], cachedAt: new Date().toISOString(), organizationId: user.organization.id, } console.log('✅ GET_SELLER_STATS_CACHE DOMAIN SUCCESS:', { organizationId: user.organization.id, period: args.period, hasCachedData: false, // В реальной системе проверить наличие кеша }) return mockCacheData } catch (error: any) { console.error('❌ GET_SELLER_STATS_CACHE DOMAIN ERROR:', error) return { period: args.period, dateFrom: args.dateFrom, dateTo: args.dateTo, productsData: null, productsTotalSales: null, totalRevenue: null, totalOrders: null, averageOrderValue: null, topProducts: null, cachedAt: null, organizationId: null, } } }), }, Mutation: { // Сохранение кеша статистики селлера saveSellerStatsCache: withAuth(async ( _: unknown, { input, }: { input: { period: string dateFrom?: string | null dateTo?: string | null productsData?: string | null productsTotalSales?: number | null totalRevenue?: number | null totalOrders?: number | null averageOrderValue?: number | null topProducts?: string | null } }, context: Context, ) => { console.log('🔍 SAVE_SELLER_STATS_CACHE DOMAIN MUTATION STARTED:', { userId: context.user?.id, period: input.period, dateFrom: input.dateFrom, dateTo: input.dateTo, }) try { const user = await prisma.user.findUnique({ where: { id: context.user!.id }, include: { organization: true }, }) if (!user?.organization) { throw new GraphQLError('Пользователь не привязан к организации') } const cacheKey = `seller_stats_${user.organization.id}_${input.period}_${input.dateFrom || 'null'}_${input.dateTo || 'null'}` console.log('💾 Saving cache:', { cacheKey, dataSize: JSON.stringify(input).length, }) // TODO: Реализовать сохранение в отдельной таблице кеша или Redis // await saveToCache(cacheKey, input) console.log('✅ SAVE_SELLER_STATS_CACHE DOMAIN SUCCESS:', { organizationId: user.organization.id, period: input.period, cacheKey, }) return { success: true, message: 'Кеш статистики селлера сохранен', cachedAt: new Date().toISOString(), } } catch (error: any) { console.error('❌ SAVE_SELLER_STATS_CACHE DOMAIN ERROR:', error) return { success: false, message: error.message || 'Ошибка при сохранении кеша статистики', cachedAt: null, } } }), }, } console.warn('🔥 ANALYTICS DOMAIN МОДУЛЬ ЭКСПОРТЫ ГОТОВЫ')